JVM之类加载过程

目录

一、引言

二、类加载过程

1.Load 

3.Init

三、案例

1.第1处说明(new关键字与newInstance()方法区别)

2.第 2 处说明(使用类似的方式可获取其他声明如注解、方法等)

3.第 3 处说明(private 成员在类外依然可以修改)

四、类加载器结构

1.最高层Bootstrap

2.第二层Platform ClassLoader(JDK9)

3.第三层Application ClassLoader

五、类加载过程(双亲委派模型)

六、自定义类加载器

1.需要自定义类加载器情形

2.实现自定义类加载器的步骤


一、引言

在冯·诺依曼定义的计算机模型中,任何程序都需要加载到内存才能与 CPU 进行交流。字节码 .class 文件同样需要加载到内存中,才可以实例化类。"兵马未动,粮草先行。" ClassLoader 正是准备粮草的先行军,它的使命就是提前加载 .class 类文件到内存中。在加载类时,使用的是 Parents Delegation Model ,译为双亲委派模型,这个译名有些不妥。如果意译的话,则译作 “溯源委派加载模型”更加贴切。

二、类加载过程

Java 的类加载器是一个运行时核心基础设施模块,如下图所示,主要是在启动之初进行类的 Load LinkInit , 即加载、链接、初始化。

1.Load 

第一步, Load 阶段读取类文件产生二进制流,并转化为特定的数据结构,初步校验 cafe babe 魔法数、常量池、文件长度、是否有父类等,然后创建对应类的 java.lang.Class 实例。

第二步, Link 阶段包括验证、准备、解析三个步骤。验证是更详细的校验,比如 final 是否合规、类型是否正确、静态变量是否合理等;准备阶段是为静态变量分配内存,并设定默认值解析类和方法确保类与类之间的相互引用正确性完成内存结构布局

3.Init

第三步, Init 阶段执行类构造器<clinit> 方法,如果赋值运算是通过其他类的静态方法来完成的,那么会马上解析另外一个类,在虚拟机栈中执行完毕后通过返回值进行赋值。

三、案例

类加载是一个将 .class 字节码文件实例化成 Class 对象并进行相关初始化的过程。在这个过程中, JVM 会初始化继承树上还没有被初始化过的所有父类,并且会执行这个链路上所有未执行过的静态代码块、静态变量赋值语句等。某些类在使用时,也可以按需由类加载器进行加载。

全小写的 class 是关键字用来定义类,而首字母大写的 Class 它是所有 class 的类。这句话理解起来有难度,是因为类已经是现实世界中某种事物的抽象,为什么这个抽象还是另外个类 Class 的对象?示例代码如下:

public class ClassTest {
    // 数组类型有一个魔法属性:length 来获取数组长度
    private static int[] array = new int[3];
    private static int length = array.length;

    //任何小写 class 的定义的类,也有一个魔法属性:class,来获取此类的大写 Class 类对象
    private static Class<One> one = One.class;
    private static Class<Another> another = Another.class;

    public static void main(String[] args) throws Exception {
        // 通过 newInstance 方法创建 One 和 Another 的类对象 (第1处)
        One oneObject = one.newInstance();
        oneObject.call();

        Another anotherObject = another.newInstance();
        anotherObject.speak();

        // 通过 one 这个大写的 Class 对象,获取私有成员属性对象 Filed (第2处)
        Field privateFiledInOne = one.getDeclaredField("inner");

        // 设置私有对象的属性可以访问和修改 (第3处)
        privateFiledInOne.setAccessible(true);

        privateFiledInOne.set(oneObject, "world changed.");
        // 成功修改类的私有属性 inner 变量值为 world changed.
        System.out.println(oneObject.getInner());
    }
}

class One {
    private String inner = "time files.";

    public void call() {
        System.out.println("hello world.");
    }

    public String getInner() {
        return inner;
    }
}

class Another {
    public void speak() {
        System.out.println("easy coding.");
    }
}

1.第1处说明(new关键字newInstance()方法区别

Class 类下的newInstance()在JDK9中已经置为过时,使用getDelaredConstructor().newlnstance()的方式。这里着重说明一下 new 与newInstance 的区别。

new 关键字强类型校验可以调用任何构造方法,在使用 new 操作的时候,这个类可以没有被加载过

而 Class 类下的 newInstance() 方法弱类型只能调用无参数构造方法,如果没有默认构造方法,就抛出InstantiationException 异常,如果此构造方法没有权限访问,则抛出IllegalAccessException 异常。Java 通过类加载器类的实现类的定义进行解耦,所以是实现面向接口编程、依赖倒置的必然选择。

Java中工厂模式经常使用 newInstance() 方法来创建对象,因此从为什么要使用工厂模式上可以找到具体答案:

(1)初始级别,其中 ExampleInterface 是 Example 的接口

    class c = Class.forName("Example");
    factory = (ExampleInterface)c.newInstance(); 

(2)进阶级别,将需要创建对象的类名定义成字符串,作为参数放入forName() 方法中

    String className = "Example";
    class c = Class.forName(className);
    factory = (ExampleInterface)c.newInstance();

(3)变身级别,已经不存在 Example 的类名称,无论 Example 类怎么变化,上述代码不变,甚至可以更换 Example 的兄弟类 Example2、Example3、Example4……,只要他们实现 ExampleInterface 接口就可以

    //从xml 配置文件中获得字符串    
    String className = readfromXMlConfig();
    class c = Class.forName(className);
    factory = (ExampleInterface)c.newInstance();

 从JVM的角度看,我们使用关键字new创建一个类的时候,这个类可以没有被加载。但是使用newInstance()方法的时候,就必须保证:1、这个类已经加载;2、这个类已经连接了。而完成上面两个步骤的正是Class的静态方法forName()所完成的,这个静态方法调用了启动类加载器,即加载java API的那个加载器。

 现在可以看出,newInstance()实际上是把new这个方式分解为两步,即首先调用Class加载方法加载某个类,然后实例化。 这样分步的好处是显而易见的。我们可以在调用class的静态加载方法forName时获得更好的灵活性,提供给了一种降耦的手段。

2.第 2 处说明(使用类似的方式可获取其他声明如注解、方法等)

3.第 3 处说明(private 成员在类外依然可以修改

private 成员在类外是否可以修改?通过 setAccessible( true)操作,即可使用大写 Class 类的 set 方法修改其值。如果没有这一步,则抛出如下异常

Exception in thread "main" java.lang.IllegalAccessException: Class example.ClassTest can not access a member of class example.One with modifiers "private"

 通过以上示例,对于 Class 这个"类中之王",不会有恐惧心理了吧?那么回到类加载中,类加载器是如何定位到具体的类文件并读取的呢?

四、类加载器结构

类加载器类似于原始部落结构,存在权力等级制度。

1.最高层Bootstrap

最高的一层是家族中威望最高的 Bootstrap ,它是在 JVM 启动时创建的,通常由与操作系统相关的本地代码实现,是最根基的类加载器,负责装载最核心的 Java 类,比如 ObjectSystem String 等;它是通过 C/C++ 实现的,并不存在于 JVM 体系内。

2.第二层Platform ClassLoader(JDK9)

第二层是在 JDK9 版本中,称为 Platform ClassLoader ,即平台类加载器,用以加载一些扩展的系统类,比如 XML加密压缩相关的功能类等 ,而 JDK9 之前的加载器是Extension ClassLoader。第二层平台类加载器是通过Java 语言实现

3.第三层Application ClassLoader

第三层是 Application ClassLoader 的应用类加载器,主要是加载用户定义的 CLASSPATH 路径下的类。第三层应用类加载器是通过Java 语言实现

第二、三层类加载器为 Java 语言实现,用户也可以自定义类加载器。查看本地类加载器的方式如下(编译环境JDK7):

    ClassLoader classLoader = ClassTest.class.getClassLoader();
    // 当前正在使用的类加载器    
    System.out.println(classLoader);
    ClassLoader parent = classLoader.getParent();
    // 当前正在使用的类加载器的父加载器
    System.out.println(parent);
    ClassLoader grandparent = parent.getParent();
    // 当前正在使用的类加载器的祖父加载器
    System.out.println(grandparent);

 打印结果如下: 

sun.misc.Launcher$AppClassLoader@146ccf3e
           sun.misc.Launcher$ExtClassLoader@7399f9eb
           null

因为在JDK7环境中,所以第二层打印结果为ExtClassLoader。AppClassLoader 的 Parent 为 Bootstrap,它是通过 C/C++实现的,并不存在于 JVM 体系内,所以输出为 null,类加载器具有等级制度,但是并非继承关系,以组合的方式来复用父加载器的功能,这也符合组合优先原则,详细的双亲委派模型见下文。
 

五、类加载过程(双亲委派模型

低层次的当前类加载器不能覆盖更高层次类加载器已经加载的类。如果低层次的类加载器想加载一个未知类,要非常礼貌地向上逐级询问:"请问,这个类已经加载了吗?" 被询问的高层次类加载器会自问两个问题,第一,我是否已加载过此类?第二,如果没有,是否可以加载此类?只有当所有高层次类加载器在两个问题上的答案均为"否"时才可以让当前类加载器加载这个未知类

如上图所示,左侧绿色箭头向上逐级询问是否已加载此类,直至 Bootstrap ClassLoader ,然后向下逐级尝试是否能够加载此类,如果都加载不了则通知发起加载请求的当前类加载器, 准予加载。在右侧的三个小标签里,列举了此层类加载器主要加载的代表性类库 , 事实上不止于此。通过如下代码可以查看 Bootstrap 所有已经加载的类库:

    URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
    for (URL urL : urLs) {
        System.out.println(urL.toExternalForm());
    }

执行结果如下:

file:/C:/Software/JDK1.7/jre/lib/resources.jar
           file:/C:/Software/JDK1.7/jre/lib/rt.jar
           file:/C:/Software/JDK1.7/jre/lib/sunrsasign.jar
           file:/C:/Software/JDK1.7/jre/lib/jsse.jar
           file:/C:/Software/JDK1.7/jre/lib/jce.jar
           file:/C:/Software/JDK1.7/jre/lib/charsets.jar
           file:/C:/Software/JDK1.7/jre/lib/jfr.jar
           file:/C:/Software/JDK1.7/jre/classes

Bootstrap 加载的路径可以追加,不建议修改或删除原有加载路径 。 在 JVM 中增加如下启动参数,则能通过Class.forName 正常读取到指定类,说明此参数可以增加 Bootstrap 的类加载路径:

-Xbootclasspath/a:/Users/mark/Java/src

 如果想在启动时观察加载了哪个 jar 包中的哪个类,可以增加 -XX:+TraceClassLoading参数,此参数在解决类冲突时非常实用,毕竟不同的 JVM 环境对于加载类的顺序并非是一致的。有时想观察特定类的加载上下文,由于加载的类数量众多,调试时很难捕捉到指定类的加载过程,这时可以使用条件断点功能。比如,想查看 HashMap 的加载过程,在 loadClass 处打个断点,并且在 condition 框内输入var1.equals("java.util.HashMap")条件。

六、自定义类加载器

在明白了类加载器的实现机制后,知道双亲委派模型并非强制模型(我们可以对其中加载路径进行修改删除,而非一定要加载某些包),用户可以自定义类加载器,在什么情况下需要自定义类加载器呢?

1.需要自定义类加载器情形

(1)隔离加载类

在某些框架内进行中间件与应用的模块隔离, 把类加载到不同的环境。比如,阿里内某容器框架通过自定义类加载器确保应用中依赖的 jar 包不会影响到中间件运行时使用的 jar 包

(2)修改类加载方式

类的加载模型并非强制,除 Bootstrap 外,其他的加载并非定要引入,或者根据实际情况在某个时间点进行按需进行动态加载

(3)扩展加载源

比如从数据库、网络 ,甚至是电视机机顶盒进行加载。

(4)防止源码泄露

Java 代码容易被编译和篡改,可以进行编译加密 。那么类加载器也需要自定义,还原加密的字节码。

2.实现自定义类加载器的步骤

实现自定义类加载器的步骤:继承 ClassLoader重写 findClass() 方法调用 defineClass() 方法。一个简单的类加载器实现的示例代码如下:

首先我们定义一个待加载的实体类Customer,我们把生成的Customer.class剪切到D盘目录下D:/Customer.class

public class Customer {
    private String name;
    private Integer age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Customer{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

接着我们定义一个自定义类加载器

public class CustomClassLoader extends ClassLoader {
    // 继承ClassLoader类
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 重写findClass方法
        try {
            byte[] result = getClassFromCustomPath(name);
            if (result == null) {
                throw new FileNotFoundException();
            } else {
                // 调用defineClass方法
                return defineClass(name, result, 0, result.length);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        throw new ClassNotFoundException();
    }

    private byte[] getClassFromCustomPath(String name) throws IOException {
        // 从自定义路径中加载指定类
        // 这里要读入.class的字节,因此要使用字节流
        FileInputStream fis = new FileInputStream(new File("D:/Customer.class"));
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);

        while (true)
        {
            int i = fc.read(by);
            if (i == 0 || i == -1) {
                break;
            }
            by.flip();
            wbc.write(by);
            by.clear();
        }

        fis.close();

        return baos.toByteArray();
    }

}

测试类

public class TestMethod {
    public static void main(String[] args) {
        CustomClassLoader customClassLoader = new CustomClassLoader();
        try {
            Class<?> clazz = Class.forName("example.Customer", true, customClassLoader);
            Object obj = clazz.newInstance();
            System.out.println(obj.getClass().getClassLoader());
            System.out.println(obj.getClass().getClassLoader().getParent());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

测试结果

example.CustomClassLoader@4e543c44
           sun.misc.Launcher$AppClassLoader@146ccf3e

注意点,很可能你打印出来的是

sun.misc.Launcher$AppClassLoader@146ccf3e
           sun.misc.Launcher$ExtClassLoader@7399f9eb

那是因为你没有删除项目路径下编译生成的 Customer.class 文件,你的Eclipse或者IDEA自动编译Customer类(此时是用的AppClassLoader编译)后,生成了Customer.class 文件,当你准备用自定义类加载器的时候,根据双亲委任模型,会向上级请示是否已经加载过此类,显然,当询问到 AppClassLoader 加载器时得到的答案为:是。因此此时自定义类加载器变不会生效

由于中间件一般都有自己的依赖 jar 包,在同一个工程内引用多个框架时,往往被迫进行类的仲裁。按某种规则 jar 包的版本被统一指定,导致某些类存在包路径、类名相同的情况, 就会引起类冲突,导致应用程序出现异常。主流的容器类框架都会
自定义类加载器
实现不同中间件之间的类隔离,有效避免了类冲突

### JVM加载机制详细过程 JVM(Java虚拟机)的类加载机制是Java程序运行的基础之一,它负责将编译后的 `.class` 文件加载到内存中,并对这些数据进行校验、解析和初始化,最终形成可以直接被JVM使用的Java类型。整个过程可以分为七个阶段:加载、验证、准备、解析、初始化、使用和卸载,其中前五个阶段统称为类加载过程。 #### 加载(Loading) 加载阶段是类加载过程的第一个阶段,主要任务是通过类的全限定名获取其对应的二进制字节流,并将这些字节流转化为方法区中的运行时数据结构。同时,JVM会在堆中创建一个 `java.lang.Class` 对象,作为该类访问数据的入口。 #### 验证 验证阶段的目的是确保Class文件的字节流中包含的信息符合JVM规范的要求,避免JVM受到恶意代码的攻击。验证过程包括文件格式验证、元数据验证、字节码验证以及符号引用验证。 #### 准备 准备阶段的主要任务是为类的静态变量分配内存,并为其设置初始值。这个初始值通常是零值(如 `0`、`null`),而不是代码中显式赋值的值。显式赋值会在初始化阶段完成。 #### 解析 解析阶段是将常量池中的符号引用替换为直接引用的过程。符号引用是以一组符号来描述所引用的目标,而直接引用则是直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。解析阶段并不一定在初始化之前完成,某些解析操作可能会延迟到初始化之后进行,这是为了支持Java的动态绑定特性。 #### 初始化 初始化阶段是类加载过程的最后一个阶段,它负责执行类构造器 `<clinit>` 方法。`<clinit>` 方法是由编译器自动收集类中的所有静态变量的赋值动作和静态代码块中的语句合并生成的,代码会按照书写顺序依次执行。如果类的父类尚未初始化,JVM会先触发其父类的初始化过程,以确保继承链的完整性。此外,JVM会确保 `<clinit>` 方法在多线程环境中被正确加锁和同步,以保证线程安全。 例如,在以下代码中,`ConstClass` 类的静态代码块不会在 `NotInit` 类的 `main` 方法执行时被触发初始化,因为 `HELLOWORLD` 是一个 `static final` 常量,其值在编译期就已经确定,并直接嵌入到调用类的常量池中: ```java public class ConstClass { static { System.out.println("常量类初始化!"); } public static final String HELLOWORLD = "hello world!"; } public class NotInit { public static void main(String[] args) { System.out.println(ConstClass.HELLOWORLD); } } ``` #### 类加载JVM中的类加载器分为以下几类: 1. **启动类加载器(Bootstrap ClassLoader)**:负责加载JVM核心类库(如 `rt.jar` 中的类),通常是用C++实现的,而不是Java代码。 2. **扩展类加载器(Extension ClassLoader)**:负责加载Java的扩展类库(如 `jre/lib/ext` 目录下的类)。 3. **系统类加载器(System ClassLoader)**:负责加载应用程序的类路径(ClassPath)上的类,通常是最常用的类加载器。 4. **自定义类加载器(Custom ClassLoader)**:用户可以通过继承 `ClassLoader` 类来实现自定义的类加载逻辑。 类加载器之间遵循 **双亲委派模型**,即当一个类加载器收到类加载请求时,它会首先委托给父类加载器去加载,只有在父类加载器无法完成加载时,才会尝试自己加载。这种机制可以确保类的唯一性和安全性。 #### 示例代码 以下是一个简单的类加载器示例,展示了如何通过自定义类加载加载类: ```java public class CustomClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = loadClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } } private byte[] loadClassData(String className) { // 实现类的加载逻辑,例如从文件或网络读取字节码 return new byte[0]; } } ``` #### 相关问题 - 类加载的双亲委派模型是如何工作的? - 如何实现一个自定义的类加载器? - 为什么 `static final` 常量不会触发类的初始化? - 类加载过程中验证阶段的作用是什么? - 解析阶段为什么可以延迟到初始化之后?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值