Skip to content

类加载过程:Linking(验证/准备/解析)

Linking:加载之后的「质检与翻译」

字节码被加载到内存后,JVM 并不能直接使用。它需要先经过 Linking(链接)阶段的检查和转换。Linking 分三步:验证(Verification)准备(Preparation)解析(Resolution)

Loading(加载)


Linking(链接)
  ├─ Verification(验证)── 字节码合法吗?安全吗?
  ├─ Preparation(准备)── 分配内存,设默认值
  └─ Resolution(解析)── 符号引用 → 直接引用


Initialization(初始化)


     使用

Verification:字节码的安检门

验证是 JVM 的安全防线。它要确保加载进来的字节码不会破坏 JVM 的完整性。

想象一下:如果一段恶意代码伪造了一个 Object 引用,让它看起来像是一个数组指针,然后用它去访问任意内存——没有验证的话,JVM 就直接崩溃了。

验证的四个阶段

JVM 的字节码验证器会对字节码进行以下检查:

1. 文件格式验证

检查字节码文件是否符合 Class 文件格式规范。

java
// 检查项:
// - 魔数是否为 0xCAFEBABE
// - 主版本号和次版本号是否在当前 JVM 支持的范围内
// - 常量池中是否有不支持的常量类型
// - ...

如果这一步失败,你会遇到 java.lang.ClassFormatError

2. 元数据验证

对字节码描述的元信息进行语义分析,确保符合 Java 语言规范。

java
// 检查项:
// - 这个类是否有父类(除 Object 外都应该有)
// - 继承关系是否合法(final 类不能被继承)
// - 方法签名是否冲突
// - 抽象方法是否全部实现
// - ...

这一步会捕获大部分的代码错误。比如:

java
public class BadClass extends String {
    // 编译不会报错(语法上合法)
    // 但运行时会报 ClassFormatError
    // 因为 String 是 final 的,不能被继承
}

3. 字节码验证

这是最复杂的一步。通过数据流分析和控制流分析,确保字节码不会做出危险操作。

java
// 检查项:
// - 操作数栈不会溢出或下溢
// - 本地变量使用前已初始化
// - 方法调用参数类型匹配
// - 不会访问非法内存(用非法引用访问对象)
// - ...

这个阶段最经典的问题是:栈操作的一致性

java
// 恶意字节码(会被验证器拦截)
iload_1     // 从局部变量槽 1 加载 int
aload_2     // 把它当作对象引用使用 —— 验证失败!

HotSpot 在 JDK 8 之后对验证进行了优化(StackMapTable),用预设的类型信息替代了全量分析,大幅加快了类的加载速度。

4. 符号引用验证

在解析阶段之前,JVM 需要检查符号引用的目标是否真实存在、是否具有访问权限。

java
// 检查项:
// - 类/字段/方法是否存在
// - 是否有足够的访问权限(public / protected / private)
// - 是否是 final 类的非 final 方法
// - ...

这一步会抛出 IllegalAccessErrorNoSuchFieldErrorNoSuchMethodError 等错误。

Preparation:分配内存,设默认值

准备阶段是分配内存并设置默认值的阶段。

关键:默认值不是初始值

这是最容易搞错的地方。

java
public class PreparationDemo {
    // 准备阶段:value 被分配内存,设置为默认值 0
    // 注意:此时还没有执行 static {} 中的赋值
    public static int value = 123;

    // 准备阶段:str 被分配内存,设置为默认值 null
    public static String str = "hello";

    // 准备阶段:arr 被分配内存,设置为默认值 null
    public static int[] arr = new int[10];

    static {
        // 初始化阶段:这里才会执行真正的赋值
        // value 才会被设为 123
        value = 456;
        System.out.println("Static block executed, value = " + value);
    }
}

基本类型的默认值

类型默认值
int0
long0L
short0
byte0
char'\u0000'
booleanfalse
float0.0f
double0.0d
referencenull

常量(static final)的特殊情况

如果一个字段是 static final 且是编译期常量(字面量),在准备阶段就会直接赋值,不需要等到初始化。

java
public class ConstantDemo {
    // 编译期常量:在编译时值就已知
    // 准备阶段直接赋值为 123,不需要等到初始化
    public static final int CONST = 123;

    // 不是编译期常量:比如 new Random().nextInt()
    // 准备阶段只赋默认值 null
    public static final String STR = new String("hello");
}

这是编译器优化的空间:static final 常量在编译时就被嵌入到使用它的地方,不需要运行时解析。

Resolution:符号引用变直接引用

解析是把符号引用转换为直接引用的过程。

符号引用 vs 直接引用

  • 符号引用(Symbolic Reference):用字符串表示的引用,不依赖具体内存地址。在 Class 文件中,所有对类、字段、方法的引用都是符号引用。

    • "java.lang.String" 是类的符号引用
    • "java.io.PrintStream.out" 是字段的符号引用
    • "java.lang.Object.<init>()V" 是方法的符号引用
  • 直接引用(Direct Reference):直接指向目标的指针或句柄,是运行时确定的真实内存地址。

java
// 源代码中:
public class ResolutionDemo {
    Object obj = new Object();
    // "Object" 是符号引用
    // 解析后:obj 的类型槽中存储的是 Object 类在方法区中的实际地址
}

// 字节码中:
//   new #2          // #2 是常量池中的符号引用索引
//   dup
//   invokespecial #3 // #3 也是符号引用索引

解析的时机:早期绑定 vs 晚期绑定

JVM 规范允许在初始化阶段之后再进行解析,这叫晚期绑定(Lazy Resolution)

  • 非虚方法(private、static、final、构造器):在解析时就知道目标是哪个,可以提前解析
  • 虚方法(instance 方法):编译时不知道运行时实际类型,可能延迟到第一次调用时解析

不过 HotSpot 的实现基本上是激进解析:能解析的尽量在类加载时就解析完,以提高后续调用效率。

解析的几种情况

符号引用解析目标失败错误
类或接口直接引用:方法区中该类的类对象NoClassDefFoundError
字段直接引用:字段在类中的内存偏移量NoSuchFieldError
方法直接引用:方法的入口地址或 vtable 槽位NoSuchMethodError
接口方法直接引用:接口方法表中的槽位AbstractMethodError

三个阶段的执行者

Verification(验证)──→ 字节码验证器(native 代码)
Preparation(准备)──→ JVM 内存管理模块(设置默认零值)
Resolution(解析)──→ JVM 运行时(符号引用解析)

这三个阶段都没有执行任何 Java 代码,它们只是把字节码「准备好」。

本节小结

Linking 阶段的三步各有分工:

阶段任务关键词
Verification检查字节码是否合法、安全验证器、安全检查
Preparation分配内存,设置默认值零值零值、基本类型
Resolution符号引用 → 直接引用符号引用、内存地址

真正给变量赋初始值、执行静态代码块,是在下一节——类加载过程:Initialization

加载 → 链接 → 初始化的完整流程图

类字节码

    │ Loading

读取到内存

    │ Verification(检查格式、元数据、字节码、符号引用)

分配内存 + 设置默认值(零值)

    │ Preparation

符号引用 → 直接引用

    │ Resolution

Initialization(执行 clinit)


类可以使用

基于 VitePress 构建