Skip to content

Java 类加载机制介绍

Java 的类加载机制是 Java 虚拟机(JVM)的核心组成部分,它负责在运行时将 .class 文件中描述的类数据加载到内存中,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。这种动态加载的特性是 Java 语言实现平台无关性和强大扩展性的关键。

一个类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括七个阶段。其中前五个阶段构成了类加载的全过程。一个类的完整生命周期包括加载(Loading)、链接(Linking)、初始化(Initialization)、使用(Using)和卸载(Unloading)这五个核心阶段,其中链接(Linking)又细分为验证(Verification)、准备(Preparation)和解析(Resolution)三个过程。这些阶段的开始顺序是确定的,但它们并非“按序执行完毕”,而是通常互相交叉地混合进行。

1. 加载 (Loading)

这是类加载过程的第一个阶段。在此阶段,JVM 主要完成三项工作:

  1. 获取字节流:通过一个类的全限定名(Fully Qualified Name),来获取定义这个类的二进制字节流。

  2. 转换数据结构:将这个字节流所代表的静态存储结构,转化为方法区(Metaspace/PermGen)中的运行时数据结构。

  3. 创建Class对象:在 Java 堆(Heap)中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

2. 链接 (Linking)
a. 验证 (Verification)

此阶段的目的是确保被加载的 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。这是一个非常重要的过程,但不是必须的(可以通过 -Xverify:none 参数关闭,以缩短类加载时间)。

验证主要包含四个阶段的检验:

  1. 文件格式验证:验证字节流是否符合 Class 文件格式规范,例如是否以魔数 0xCAFEBABE 开头,主、次版本号是否在当前虚拟机可接受范围之内。

  2. 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求。例如,这个类是否有父类(除了 java.lang.Object 之外)。

  3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这是整个验证过程中最复杂的一个阶段。

  4. 符号引用验证:对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,确保解析阶段能正常执行。

b. 准备 (Preparation)

准备阶段是正式为 类变量(即静态变量,被 static 修饰的变量) 分配内存并设置其 初始值 的阶段。

关键点:

  • 分配对象:此阶段只处理类变量,不包括实例变量。实例变量是在对象实例化时随着对象一起分配在 Java 堆中。

  • 初始值:通常情况下,初始值是数据类型的“零值”(如 00Lnullfalse 等),而不是在 Java 代码中显式赋予的值。

    • 例如:public static int value = 123; 在准备阶段后,value 的值是 0 而不是 123。把 value 赋值为 123putstatic 指令,是在初始化阶段才会执行。
  • 特殊情况:如果类字段的字段属性表中存在 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 虚拟机规范严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始),这六种行为称为对一个类的“主动使用”。

  1. 遇到 newgetstaticputstaticinvokestatic 这四条字节码指令时。对应的 Java 代码场景是:

    • 使用 new 关键字实例化对象。

    • 读取或设置一个类型的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)。

    • 调用一个类型的静态方法。

  2. 使用 java.lang.reflect 包的方法对类型进行反射调用的时候。

  3. 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。

  5. 当使用 JDK 7 新加入的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStaticREF_putStaticREF_invokeStatic 的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。

  6. 当一个接口中定义了 JDK 8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。