JVM类加载加载链接初始化

wen java案例 1

本文目录导读:

JVM类加载加载链接初始化

  1. 第一阶段:加载 (Loading)
  2. 第二阶段:链接 (Linking)
  3. 第三阶段:初始化 (Initialization)
  4. 总结对比表
  5. 经典面试问题

这是关于 JVM(Java 虚拟机)类加载机制中 加载、链接、初始化 三个核心阶段的深度解析,这是 Java 程序员面试高频考点,也是理解 Java 虚拟机运行机制的基础。

我们将分为三个阶段详细讲解,并附上关键图示描述和代码示例。


一个 Java 类从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期包括七个阶段: 加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载

加载、验证、准备、解析、初始化 这五个阶段是顺序发生的(但解析阶段在某些情况下可以在初始化之后开始,为了支持动态绑定)。

我们今天重点解析前三步:加载、链接(包含验证、准备、解析)、初始化


第一阶段:加载 (Loading)

定义:查找并加载类的二进制数据(字节码)。

JVM 在这个阶段具体做了什么?

  1. 通过类的全限定名获取定义此类的二进制字节流
    • 通常是从 .class 文件读取,但也可以从 JAR、WAR 包、网络、动态代理生成、JSP 文件(编译后)等获取。
  2. 将字节流所代表的静态存储结构转化为方法区的运行时数据结构

    类信息(字段、方法、接口、常量池等)存储在方法区。

  3. 在内存中生成一个代表这个类的 java.lang.Class 对象

    这个对象存储在堆中,作为方法区这个类的各种数据的访问入口。

关键角色类加载器 (ClassLoader)

  • 双亲委派模型:当一个类加载器收到加载请求,它首先不会自己尝试加载,而是委派给父类加载器,只有父类加载器无法加载时,子类才会尝试自己加载。
  • 三层类加载器
    • Bootstrap ClassLoader:加载 JAVA_HOME/lib 目录中的核心库(如 rt.jar)。
    • Extension ClassLoader:加载 JAVA_HOME/lib/ext 目录中的扩展库。
    • Application ClassLoader:加载用户类路径 (classpath) 上的类。

示例:当你写 System.out.println() 时,System.class 是由 Bootstrap ClassLoader 加载的。


第二阶段:链接 (Linking)

链接阶段分为三个子步骤:验证、准备、解析。

验证 (Verification)

目的:确保被加载的类的字节流符合 JVM 规范,且不会危害虚拟机自身安全,这是最严格的一步。

主要动作

  • 文件格式验证:验证魔数 (0xCAFEBABE)、版本号等。
  • 元数据验证:检查类是否有父类(Object除外),父类是否可继承,是否实现了所有抽象方法等。
  • 字节码验证:最复杂的一步,通过数据流和控制流分析,确保字节码指令序列合法(如:操作数栈不会溢出,跳转指令不会指向方法体外等)。
  • 符号引用验证:发生在解析阶段之前,确保后面的解析能正常进行(如:检查类中引用的类/方法/字段是否存在、是否有访问权限)。

例子:如果修改一个 .class 文件的魔数或篡改字节码指向一个非法内存地址,JVM 在验证阶段就会抛出 java.lang.VerifyError

准备 (Preparation)

目的:为类变量(静态变量 static)分配内存,并设置默认初始值(零值)。

注意

  • 内存分配:仅包含 static 变量,不包含实例变量(实例变量在对象实例化时分配在堆中)。
  • 零值int → 0,boolean → false,Object → null。
  • 例外情况:如果类变量被 final 修饰,即常量,那么在准备阶段就会被赋值为用户指定的值。

代码演示

public class Example {
    public static int value = 123;       // 准备阶段:value = 0
    public static final int CONST = 456; // 准备阶段:CONST = 456(final 特殊处理)
    private String name;                  // 准备阶段:不分配内存
}

在准备阶段结束后,value 的值是 0,而不是 123,赋值操作 value = 123 发生在后面的初始化阶段(调用 <clinit> 方法)。

解析 (Resolution)

目的:将常量池内的符号引用 (Symbolic Reference) 替换为直接引用 (Direct Reference)

  • 符号引用:以一组符号(如 CONSTANT_Class_info, CONSTANT_Methodref_info)来描述所引用的目标,与 JVM 内存布局无关。
  • 直接引用:可以直接指向目标的指针、偏移量或句柄,与 JVM 内存布局相关。

解析动作针对:类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符。

关键点:JVM 规范允许在 初始化阶段 之后才执行部分解析(延迟解析),只要保证在解析操作真正发生之前,符号引用都是可用的即可,这支持了 Java 的动态绑定(多态)。


第三阶段:初始化 (Initialization)

定义:执行类构造器方法 <clinit>() 的过程。

发起时机(主动引用):JVM 规范严格规定了只有以下 6 种情况才会立即对类进行初始化(加载、验证、准备自然在此之前发生):

  1. new 关键字实例化对象、读取或设置一个类的静态字段(不包括被 final 修饰的常量)、调用一个类的静态方法。
  2. 反射调用:使用 Class.forName()java.lang.reflect.Method.invoke()
  3. 启动类:当初始化一个类,如果其父类还没有初始化,需先触发其父类的初始化。
  4. main 方法:虚拟机启动时,包含 main() 方法的那个类。
  5. MethodHandleVarHandlejava.lang.invoke.MethodHandle 解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 等时。
  6. 接口默认方法:如果一个接口定义了 default 方法,那么直接或间接实现该接口的类初始化时,该接口需要先初始化。

<clinit>() 方法详解

  • 由编译器自动收集类中所有类变量(static)的赋值动作静态语句块(static {}) 中的语句合并而成。
  • 收集顺序:按语句在源文件中出现的顺序。
  • 该方法不需要显示调用,父类的 <clinit>() 会先于子类的执行。
  • 线程安全:JVM 会保证一个类的 <clinit>() 方法在多线程环境中被正确地加锁、同步,如果有多个线程同时初始化一个类,只有一个线程会执行 <clinit>(),其他线程必须等待。

代码示例 - 初始化触发

class Parent {
    public static int A = 1;
    static {
        A = 2;
    }
}
class Sub extends Parent {
    public static int B = A; // 子类访问父类静态变量
}
public class InitOrder {
    public static void main(String[] args) {
        // 主动引用1:new对象
        Parent p = new Parent(); // 触发Parent的初始化
        // 主动引用4:main方法所在类的初始化(此处就是InitOrder)
        // 主动引用3:通过子类引用父类的静态字段,不会导致子类初始化
        System.out.println(Sub.B); // 输出:2(只初始化了Parent,子类Sub没有被初始化)
    }
}

总结对比表

阶段 主要任务 生成产物 是否必须按顺序 关键点
加载 查找并读入字节流 方法区数据结构 + 堆中的 Class 对象 双亲委派模型
链接-验证 检查字节流安全与合法性 魔数检查、字节码校验
链接-准备 静态变量分配内存并设零值 静态字段内存空间 final 常量直接赋初始值
链接-解析 符号引用 -> 直接引用 直接指针/偏移量 可延迟 支持多态的动态绑定
初始化 执行 <clinit>() 方法 静态字段的最终赋值 执行主动引用,线程安全

经典面试问题

  1. 什么时候会发生类的初始化?(前文6种主动引用)
  2. Class.forName()ClassLoader.loadClass() 有什么区别?
    • forName() 默认会执行初始化(执行 <clinit>),并触发静态代码块。
    • loadClass() 默认只执行加载阶段(不进行链接和初始化),常用于延迟加载或框架中(如 Spring IoC 容器的 BeanFactory)。
  3. 如果静态代码块出现异常,会怎样? JVM 会捕获该异常,并将该类标记为不可用,后续任何使用该类的尝试都会抛出 NoClassDefFoundError(而不是 ExceptionInInitializerError)。

理解这三个阶段,能帮助你清晰掌握 JVM 如何将 .class 文件变为可运行的内存对象,也是诊断类加载异常(如 ClassNotFoundException, NoClassDefFoundError, UnsupportedClassVersionError 等)的基础。

抱歉,评论功能暂时关闭!