类加载过程:Initialization
Initialization:类真正「活起来」的时刻
如果把类加载比作「盖房子」,前面 Loading 和 Linking 做的是「打地基、铺管道」,那 Initialization 才是「通电通水,让房子真正能用」的时刻。
准备阶段给 static 变量设了默认值,初始化阶段则是给它们赋予真正的初始值,并且执行 static {} 块中的代码。
<clinit>:编译器自动生成的方法
初始化方法不是程序员写的,而是编译器自动生成的。
JVM 规范规定,如果一个类有 static 变量赋值语句或 static {} 代码块,编译器就会在编译时生成一个特殊的 <clinit> 方法(class initialization)。
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 对象
new InitTrigger(); // 触发 InitTrigger 的初始化2. 访问类的静态字段
int x = InitTrigger.COUNT; // 访问 final 常量不触发,访问普通 static 字段触发注意:只有访问 static final 的编译期常量不触发初始化,访问其他任何 static 字段都会触发。
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. 调用类的静态方法
InitTrigger.doSomething(); // 触发4. 反射
Class.forName("com.example.InitTrigger"); // 触发5. 初始化子类时,先初始化父类
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 clinit6. 主类
包含 main() 方法的类,会被 JVM 首先初始化。
public class MainClass {
public static void main(String[] args) {}
}
// JVM 首先初始化 MainClass,然后执行 main 方法被动使用:不触发初始化
与「主动使用」相对的是「被动使用」——引用类的某些字段或方法,不会触发类加载或初始化。
场景一:子类引用父类的 static 字段
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。
场景二:数组类型
Parent[] parents = new Parent[10]; // 不会触发 Parent 初始化
// 触发的是数组类的加载(由 JVM 直接构造),不是 Parent 类的初始化场景三:访问编译期常量
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) | 构造器代码、实例变量赋值、构造器链调用 |
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>。
public class ThreadSafeInit {
static {
// 即使有 100 个线程同时触发初始化
// <clinit> 也只会执行一次
System.out.println("Initialization happened exactly once");
}
}
// 在 100 个线程中同时触发:
for (int i = 0; i < 100; i++) {
new Thread(() -> new ThreadSafeInit()).start();
}如果另一个线程在初始化完成前试图使用这个类,它会等待初始化完成。
本节小结
初始化阶段的核心:
- 执行
<clinit>方法:编译器自动生成,包含所有static赋值和static {}块 - 线程安全:JVM 保证
<clinit>只执行一次 - 六种触发条件:new、访问 static 字段/方法、反射、初始化子类、主类
- 被动使用不触发:子类引用父类字段、数组、编译期常量
理解初始化阶段,是理解类加载子系统全部内容的关键。接下来,我们来看 引导/扩展/系统类加载器使用,了解类加载器的层级结构。
