类加载时机


虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。


在Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的

类加载的过程

类的生命周期

【Jvm】Jvm类加载机制_加载

PS:

​NoClassDeFoundError:​​发生在类生命周期中解析阶段找不到相应的类

​ClassNotFoundException​​发生在类生命周期的加载阶段,找不到相应的类。

为支持运行时绑定,解析过程在某些情况下可在初始化之后再开始,除解析过程外的其他加载过程必须按照如图顺序开始。

加载


  1. 通过全限定类名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的​​静态存储结构转化为方法区​​的运行时数据结构。
  3. 在内存中​​生成一个代表这个类的 java.lang.Class 对象​​,作为方法区这个类的各种数据的访问入口。


验证


验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

  1. 文件格式验证:如是否以魔数 0xCAFEBABE 开头、主、次版本号是否在当前虚拟机处理范围之内、常量合理性验证等。
    此阶段保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java类型信息的要求。
  2. 元数据验证:是否存在父类,父类的继承链是否正确,抽象类是否实现了其父类或接口之中要求实现的所有方法,字段、方法是否与父类产生矛盾等。
    第二阶段,保证不存在不符合 Java 语言规范的元数据信息。
  3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。例如保证跳转指令不会跳转到方法体以外的字节码指令上。
  4. 符号引用验证:​​在解析阶段中发生​​,保证可以将符号引用转化为直接引用。


可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

准备


为​​类变量分配内存并设置类变量初始值​​,这些变量所使用的内存都将在方法区中进行分配。


​public static int value = 123​

在准备阶段过后的初始值是0 而不是123,因为这个时候没有正在的执行过java代码;而把value赋值为123的​​putstatic​​指令是程序被编译后,存放在类构造器​​clinit()​​方法中,将会在初始化的时候执行

特殊情况:final修饰

​public final static int value = 123​

被final修饰之后就是常量属性,在准备阶段虚拟机就会直接赋值,不需要等到初始化阶段;

解析


虚拟机将常量池内的符号引用替换为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。
​为支持运行时绑定,解析过程在某些情况下可在初始化之后再开始,除解析过程外的其他加载过程必须按照如图顺序开始​


​NoClassDeFoundError:​​发生在类生命周期中解析阶段找不到相应的类

初始化


到初始化阶段,才真正开始执行类中定义的 Java 程序代码,此阶段是执行 ​​clinit()​​ 方法的过程。
clinit() 方法是由编译器按语句在​​源文件中出现的顺序​​,​​依次自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并产生的​​。(不包括构造器中的语句。构造器是初始化对象的,类加载完成后,创建对象时候将调用的 init() 方法来初始化对象)


静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问

如下:

【Jvm】Jvm类加载机制_初始化_02

这样的写法是正常的; 但是如果把声明换个位置

【Jvm】Jvm类加载机制_父类_03

这样就会提示 ​​非法前向引用​​ ; 进而说明 ​​clinit() 方法是由编译器按语句在源文件中出现的顺序 依次自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并产生的​

总结一下:


  1. 初始化接单是执行 clinit() 方法的过程
  2. clinit() 方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并产生的
  3. clinit() 不需要显式调用父类,的初始化方法 clinit(),虚拟机会保证在子类的 clinit() 方法执行之前,父类的 clinit() 方法已经执行完毕,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
  4. clinit() 方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成 clinit() 方法
  5. 虚拟机会保证一个类的 clinit() 方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 clinit() 方法,其他线程都需要阻塞等待,直到活动线程执行 clinit() 方法完毕。


对于初始化阶段,虚拟机规范规定了有且只有 5 种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始)


  1. 遇到new、getstatic 和 putstatic 或 invokestatic 这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。对应场景是:使用 new 实例化对象、读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、以及调用一个类的静态方法。
  2. 对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  3. 当初始化类的父类还没有进行过初始化,则需要先触发其父类的初始化。(而一个接口在初始化时,并不要求其父接口全部都完成了初始化)
  4. 虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),
    虚拟机会先初始化这个主类。
  5. 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。


类加载器


​即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类也不相等。​


双亲委派模型

双亲委派模型的加载类逻辑可参考如下代码:

【Jvm】Jvm类加载机制_jvm_04

// 代码摘自《深入理解Java虚拟机》
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 首先,检查请求的类是否已经被加载过了
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方法来进行类加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}

自定义类加载器


  1. 继承 java.lang.ClassLoader
  2. 重写父类的 findClass() 方法,JDK 的 loadCalss() 方法在所有父类加载器无法加载的时候,会调用本身的 findClass() 方法来进行类加载,因此我们只需重写 findClass() 方法找到类的二进制数据即可。


参考文章

  1. 深入理解虚拟机

【Jvm】Jvm类加载机制_加载_05