JVM 类加载机制

2021.02.21 21:25

类的加载阶段

一个类从字节码加载到虚拟机,主要包括以下几个阶段:

类加载示意图

加载

在加载阶段,JVM 需要完成以下三件事:

  1. 通过一个类的全限定名获取定义此类的二进制字节流。

  2. 将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构。

  3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

Java 虚拟机规范并没有规定二进制流必须从字节码文件中获取,也正是因为这个原因可以通过不同的加载方式让 Java 可以实现很多动态特性,例如:

注意:

对于数组类,数组的元素类型由类加载器进行加载,但是数组类本身不通过类加载器完成,而是由 JVM 直接在内存中动态构造出来。

加载阶段与连接阶段的部分内容交叉进行,加载阶段尚未完成,连接阶段可能已经开始了。但这两个阶段的开始时间仍然保持着固定的先后顺序。

连接

连接阶段又分为验证、准备、解析。

验证

验证阶段确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

验证的过程主要有:

准备

准备阶段是正式为类变量(或称“静态成员变量”)分配内存并设置初始值的阶段。这些变量(不包括实例变量)所使用的内存都在方法区中进行分配。

初始值通常为数据的零值,假设一个类的静态变量为:

public static int value = 123;

那么变量 value 在准备阶段过后的初始值为 0 而不是 123。

这里不包含用 final 修饰的静态变量,因为 final 在编译的时候就写入到字节码文件中了,准备阶段会显式初始化,也就是直接赋值常量。

解析

解析是将常量池内的符号引用转换为直接引用的过程。

符号引用就是一组符号来描述所引用的目标。符号应用的字面量形式明确定义在《Java 虚拟机规范》的 class 文件格式中。

直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。

初始化

类初始化阶段是类加载过程的最后一步,是执行类构造器 <clinit>() 方法的过程。

<clinit>() 方法不需要定义,是编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。 如果没有静态变量和静态语句块,那么字节码文件中就不会有 <clinit>() 方法。

静态语句块中只能访问定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但不能访问。如下方代码所示:

public class Test {
    static {
        i = 0;  // 给变量赋值可以正常编译通过
        System.out.println(i);  // 这句编译器会提示“非法向前引用”
    }
    static int i = 1;
}

<clinit>() 方法不需要显式调用父类构造器,虚拟机会保证在子类的 <clinit>() 方法执行之前,父类的 <clinit>() 方法已经执行完毕。

接口中不能使用静态代码块,但接口也需要通过 <clinit>() 方法为接口中定义的静态成员变量显式初始化。但接口与类不同,接口的 <clinit>() 方法不需要先执行父类的 <clinit>() 方法,只有当父接口中定义的变量使用时,父接口才会初始化。

虚拟机会保证一个类的 <clinit>() 方法在多线程环境中被正确加锁、同步。如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 <clinit>() 方法。

“初始化”开始的时机

Java 虚拟机规范没有强制约束类加载过程的第一阶段(即:加载)什么时候开始,但对于“初始化”阶段,有着严格的规定。有且仅有 5 种情况必须立即对类进行“初始化”:

这 5 种场景中的行为称为对一个类进行主动引用,除此之外,其它所有引用类的方式都不会触发初始化,称为被动引用

类加载器(ClassLoader)的使用

ClassLoader 类是一个抽象类,其后所有的类加载器都继承自 ClassLoader(不包括启动类加载器)

ClassLoader 类的主要方法:

方法描述
getParent()返回该类加载器的超类加载器
loadClass(String name)加载名称为 name 的类,返回结果为 java.lang.Class 类的实例
findClass(String name)查找名称为 name 的类,返回结果为 java.lang.Class 类的实例
findLoadedClass(String name)查找名称为 name 的已经被加载过的类,返回结果为 java.lang.Class 类的实例
defineClass(String name,byte[] b,int off,int len)把字节数组 b 中的内容转换为一个 Java 类 ,返回结果为 java.lang.Class 类的实例
resolveClass(Class<?> c)连接指定的一个 java 类

获取 ClassLoader 的途径

方式一:获取当前类的 ClassLoader:clazz.getClassLoader()

方式二:获取当前线程上下文的 ClassLoader:Thread.currentThread().getContextClassLoader()

方式三:获取系统的 ClassLoader:ClassLoader.getSystemClassLoader()

方式四:获取调用者的 ClassLoader:DriverManager.getCallerClassLoader()

类加载器分类

系统提供了 3 种类加载器:

用户也可以通过继承 ClassLoader 实现自己的类加载器。

判断类是否相等

任意一个类,都由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都有一个独立的类名称空间。

因此,比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么这两个类就必定不相等。

这里的“相等”,包括代表类的 Class 对象的 equals() 方法、isInstance() 方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况。

双亲委派机制

双亲委派模型是描述类加载器之间的层次关系。它要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。(父子关系一般不会以继承的关系实现,而是以组合关系来复用父加载器的代码)

工作过程

  1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载请求,它并不会自己先去加载,而是把这个请求委托给父加载器进行加载。
  2. 如果父加载器还存在父加载器则进一步向上委托,最终抵达启动类加载器。
  3. 如果父加载器加载成功,则完成加载,如果父加载器加载失败,再自己尝试加载。

类加载器示意图

需要注意上图其实并不是继承关系,而是包含关系。

观察 ClassLoader 源码,即可发现 loadClass 方法中定义的双亲委派模型:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 首先,检查请求的类是否已经被加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 如果父类抛出 ClassNotFoundException 说明父类无法加载该类
            }

            if (c == null) {
                // 如果父类无法加载,再调用自身的 findClass 方法进行类加载
                long t1 = System.nanoTime();
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

为什么使用双亲委派

  1. 避免类的重复加载。
  2. 保护程序安全,防止核心 API 被随意篡改,即使用户自己实现一个 java.lang.Object,通过双亲委派,最终都会委派给最顶端的启动类加载器加载,从而使得不同加载器加载的 Object 类都是同一个。

如何打破双亲委派

自定义加载器,需要继承 ClassLoader 。如果我们不想打破双亲委派模型,就重写 findClass 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。

如果想打破双亲委派模型则需要重写 loadClass 方法,将委托父加载器的逻辑重写删掉。