JVM 規定類初始化(執行 <clinit>)僅在以下 5 種情況觸發:
new、getstatic/putstatic、invokestatic 指令Class.forName)main 方法所在類MethodHandle 解析關鍵點:<clinit> 由 JVM 鎖同步,**只執行一次**。子類初始化會遞歸觸發父類 <clinit>,**但「觸發」≠「完成」**,這正是雷區所在。
class A {
static { System.out.println("A初始化開始"); }
static B b = new B(); // 靜態字段引用子類並實例化
static int aInt = 9;
static { System.out.println("A初始化結束"); }
}
class B extends A {
static { System.out.println("B初始化開始"); }
static int bInt = 9 + A.aInt; // 依賴父類靜態字段
public B() { System.out.println("B構造器 bInt=" + bInt); }
static { System.out.println("B初始化結束 aInt=" + bInt); }
}
A初始化開始 B構造器 bInt=0 ← 關鍵!B 尚未初始化完成,bInt 為默認值 0 A初始化結束 B初始化開始 B初始化結束 aInt=18
原因:B.main 觸發 B 初始化 → 遞歸觸發 A 初始化 → A 執行 new B() → JVM 發現 B 正在初始化(持有鎖),直接實例化,**跳過 B 的 <clinit>** → 此時 bInt 未賦值,仍為 0。
A初始化開始 B初始化開始 B初始化結束 aInt=9 ← 關鍵!B 讀取 A.aInt 時 A.aInt 尚未賦值為 9 B構造器 bInt=9 A初始化結束
原因:A 初始化時 new B() 觸發 B 完整初始化 → B 讀取 A.aInt 時 A 仍在初始化中,aInt 為默認值 0 → 導致 bInt=9 而非預期 18。
static B b = new B())static 方法延遲賦值(JVM 保證方法調用前初始化完成)class A {
public A() { doSomething(); } // 構造器調用非 final/private 方法
protected void doSomething() { System.out.println("A's doSomething"); }
}
class B extends A {
private int bInt = 9;
@Override protected void doSomething() {
System.out.println("B's doSomething, bInt: " + bInt);
}
}
new B(); // 輸出:B's doSomething, bInt: 0
<init> → 首行隱式 super() 調用 A 構造器doSomething() → 動態綁定 解析為 B 的覆寫版本<init> 尚未執行到 bInt = 9(實例變量賦值在 super() 之後)bInt 為默認值 0構造器、clone()、readObject() 中絕不調用可覆寫方法(即非 final、非 private、非 static 方法)。
Java 初始化機制看似簡單,實則暗藏殺機:
最佳實踐:靜態字段不引用子類、構造器只調用 final/private 方法、複雜初始化改用靜態工廠方法。