Java 类加载机制介绍
Java 的类加载机制是 Java 虚拟机(JVM)的核心组成部分,它负责在运行时将 .class 文件中描述的类数据加载到内存中,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。这种动态加载的特性是 Java 语言实现平台无关性和强大扩展性的关键。
一个类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括七个阶段。其中前五个阶段构成了类加载的全过程。一个类的完整生命周期包括加载(Loading)、链接(Linking)、初始化(Initialization)、使用(Using)和卸载(Unloading)这五个核心阶段,其中链接(Linking)又细分为验证(Verification)、准备(Preparation)和解析(Resolution)三个过程。这些阶段的开始顺序是确定的,但它们并非“按序执行完毕”,而是通常互相交叉地混合进行。
1. 加载 (Loading)
这是类加载过程的第一个阶段。在此阶段,JVM 主要完成三项工作:
-
获取字节流:通过一个类的全限定名(Fully Qualified Name),来获取定义这个类的二进制字节流。
-
转换数据结构:将这个字节流所代表的静态存储结构,转化为方法区(Metaspace/PermGen)中的运行时数据结构。
-
创建Class对象:在 Java 堆(Heap)中生成一个代表这个类的
java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
2. 链接 (Linking)
a. 验证 (Verification)
此阶段的目的是确保被加载的 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。这是一个非常重要的过程,但不是必须的(可以通过 -Xverify:none 参数关闭,以缩短类加载时间)。
验证主要包含四个阶段的检验:
-
文件格式验证:验证字节流是否符合 Class 文件格式规范,例如是否以魔数
0xCAFEBABE开头,主、次版本号是否在当前虚拟机可接受范围之内。 -
元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求。例如,这个类是否有父类(除了
java.lang.Object之外)。 -
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这是整个验证过程中最复杂的一个阶段。
-
符号引用验证:对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,确保解析阶段能正常执行。
b. 准备 (Preparation)
准备阶段是正式为 类变量(即静态变量,被 static 修饰的变量) 分配内存并设置其 初始值 的阶段。
关键点:
-
分配对象:此阶段只处理类变量,不包括实例变量。实例变量是在对象实例化时随着对象一起分配在 Java 堆中。
-
初始值:通常情况下,初始值是数据类型的“零值”(如
0、0L、null、false等),而不是在 Java 代码中显式赋予的值。- 例如:
public static int value = 123;在准备阶段后,value的值是0而不是123。把value赋值为123的putstatic指令,是在初始化阶段才会执行。
- 例如:
-
特殊情况:如果类字段的字段属性表中存在
ConstantValue属性(即被public static final修饰),那么在准备阶段,变量就会被初始化为ConstantValue属性所指定的值。- 例如:
public static final int value = 123;在准备阶段value就会被直接赋值为123。
- 例如:
c. 解析 (Resolution)
解析阶段是虚拟机将常量池内的 符号引用 (Symbolic References) 替换为 直接引用 (Direct References) 的过程。
-
符号引用:以一组符号来描述所引用的目标,与虚拟机实现的内存布局无关。例如,
java/lang/String就是一个符号引用。 -
直接引用:可以直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,与虚拟机实现的内存布局相关。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这 7 类符号引用进行。
3. 初始化 (Initialization)
这是类加载过程的最后一步,也是真正开始执行类中定义的 Java 程序代码(字节码)的阶段。此阶段的核心是执行类构造器 <clinit>() 方法。
关于 <clinit>() 方法:
-
来源:它是由编译器自动收集类中的所有 类变量的赋值动作 和 静态语句块 (
static{}块) 中的语句合并产生的。编译器收集的顺序是由语句在源文件中出现的顺序决定的。 -
父类优先:JVM 会保证在子类的
<clinit>()方法执行前,其父类的<clinit>()方法已经执行完毕。因此,在 JVM 中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。 -
非必需:如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成
<clinit>()方法。 -
线程安全:JVM 必须保证一个类的
<clinit>()方法在多线程环境中被正确地加锁和同步。如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待。
触发初始化的时机(主动使用):
Java 虚拟机规范严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始),这六种行为称为对一个类的“主动使用”。
-
遇到
new、getstatic、putstatic或invokestatic这四条字节码指令时。对应的 Java 代码场景是:-
使用
new关键字实例化对象。 -
读取或设置一个类型的静态字段(被
final修饰、已在编译期把结果放入常量池的静态字段除外)。 -
调用一个类型的静态方法。
-
-
使用
java.lang.reflect包的方法对类型进行反射调用的时候。 -
当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
-
当虚拟机启动时,用户需要指定一个要执行的主类(包含
main()方法的那个类),虚拟机会先初始化这个主类。 -
当使用 JDK 7 新加入的动态语言支持时,如果一个
java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。 -
当一个接口中定义了 JDK 8 新加入的默认方法(被
default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。