Skip to content

类加载过程:Initialization

Initialization:类真正「活起来」的时刻

如果把类加载比作「盖房子」,前面 Loading 和 Linking 做的是「打地基、铺管道」,那 Initialization 才是「通电通水,让房子真正能用」的时刻。

准备阶段给 static 变量设了默认值,初始化阶段则是给它们赋予真正的初始值,并且执行 static {} 块中的代码。

<clinit>:编译器自动生成的方法

初始化方法不是程序员写的,而是编译器自动生成的。

JVM 规范规定,如果一个类有 static 变量赋值语句或 static {} 代码块,编译器就会在编译时生成一个特殊的 <clinit> 方法(class initialization)。

java
public class InitDemo {
    // 变量赋值 → clinit 中的一条语句
    public static int a = 10;

    // 静态代码块 → clinit 中的一个代码块
    static {
        System.out.println("Static block executed");
        b = 20;
    }

    public static int b = 30;

    // 编译器生成的 <clinit> 方法,大致等价于:
    // static {
    //     a = 10;
    //     System.out.println("Static block executed");
    //     b = 20;  // 注意:顺序很重要!
    //     b = 30;
    // }
}

关键点:clinit 方法中的语句顺序,就是源代码中出现的顺序。这意味着后面的赋值会覆盖前面的。

clinit 的执行时机

<clinit> 是线程安全的——JVM 保证 <clinit> 在多线程环境下只会被执行一次。

触发初始化的六种情况

JVM 规范定义了触发初始化的「首次主动使用」六种场景:

1. new 对象

java
new InitTrigger();  // 触发 InitTrigger 的初始化

2. 访问类的静态字段

java
int x = InitTrigger.COUNT;  // 访问 final 常量不触发,访问普通 static 字段触发

注意:只有访问 static final 的编译期常量不触发初始化,访问其他任何 static 字段都会触发。

java
public class StaticFieldAccess {
    public static final int CONST = 100;  // 编译期常量,不触发
    public static int var = 200;           // 普通 static 变量,触发

    public static void main(String[] args) {
        // 这行不触发初始化:编译器把 CONST 替换成了字面量 100
        System.out.println(CONST);
        // 这行触发初始化:需要读取 var 的值
        System.out.println(var);
    }
}

3. 调用类的静态方法

java
InitTrigger.doSomething();  // 触发

4. 反射

java
Class.forName("com.example.InitTrigger");  // 触发

5. 初始化子类时,先初始化父类

java
public class Parent {
    static { System.out.println("Parent clinit"); }
}

public class Child extends Parent {
    static { System.out.println("Child clinit"); }
}

// 访问 Child 类 → 先初始化 Parent → 再初始化 Child
// 输出:
// Parent clinit
// Child clinit

6. 主类

包含 main() 方法的类,会被 JVM 首先初始化。

java
public class MainClass {
    public static void main(String[] args) {}
}
// JVM 首先初始化 MainClass,然后执行 main 方法

被动使用:不触发初始化

与「主动使用」相对的是「被动使用」——引用类的某些字段或方法,不会触发类加载或初始化

场景一:子类引用父类的 static 字段

java
public class Parent {
    public static int value = 100;
    static { System.out.println("Parent initialized!"); }
}

public class Child extends Parent {}

public class PassiveDemo {
    public static void main(String[] args) {
        System.out.println(Child.value);  // 只初始化 Parent,不初始化 Child
        // 输出:
        // Parent initialized!
        // 100
    }
}

为什么?因为 Child.value 等价于 Parent.value,JVM 知道真正要使用的是 Parent

场景二:数组类型

java
Parent[] parents = new Parent[10];  // 不会触发 Parent 初始化
// 触发的是数组类的加载(由 JVM 直接构造),不是 Parent 类的初始化

场景三:访问编译期常量

java
public class Constants {
    public static final int VALUE = 100;  // 编译期常量
}

int x = Constants.VALUE;  // 不触发初始化
// 编译器在编译时就直接替换成了 int x = 100;

clinit 与 init 的区别

很多人容易混淆 <clinit><init>

方法全称来源触发时机内容
<clinit>class initialization编译器生成类初始化时(首次主动使用)static 变量赋值、static {}
<init>object initialization编译器生成对象构造时(new)构造器代码、实例变量赋值、构造器链调用
java
public class InitVsClinit {
    public static int staticVar = initStatic();  // clinit
    public int instanceVar = initInstance();      // init

    static { /* clinit */ }
    { /* init */ }

    public InitVsClinit() { /* init */ }  // init

    static int initStatic() { return 1; }   // clinit 调用
    int initInstance() { return 2; }        // init 调用
}

初始化的线程安全性

<clinit> 方法是 JVM 内部保证线程安全的——JVM 在初始化类时,会在内部加锁,确保只有一个线程执行 <clinit>

java
public class ThreadSafeInit {
    static {
        // 即使有 100 个线程同时触发初始化
        // <clinit> 也只会执行一次
        System.out.println("Initialization happened exactly once");
    }
}

// 在 100 个线程中同时触发:
for (int i = 0; i &lt; 100; i++) {
    new Thread(() -&gt; new ThreadSafeInit()).start();
}

如果另一个线程在初始化完成前试图使用这个类,它会等待初始化完成。

本节小结

初始化阶段的核心:

  1. 执行 <clinit> 方法:编译器自动生成,包含所有 static 赋值和 static {}
  2. 线程安全:JVM 保证 <clinit> 只执行一次
  3. 六种触发条件:new、访问 static 字段/方法、反射、初始化子类、主类
  4. 被动使用不触发:子类引用父类字段、数组、编译期常量

理解初始化阶段,是理解类加载子系统全部内容的关键。接下来,我们来看 引导/扩展/系统类加载器使用,了解类加载器的层级结构。

基于 VitePress 构建