許多教材用「仮想Initialize()方法」模糊描述初始化,實則JVM有兩個真實存在、字節碼可見、不可直接調用的特殊方法:<clinit>(類初始化)與<init>(實例初始化)。本文通過javap -c反編譯、異常堆棧定位、JLS §12.4/§12.5規範,徹底還原初始化全貌。
// Test.java
public class Test {
static int i = 100 / 0; // 靜態字段初始化拋異常
public static void main(String[] args) {
System.out.println("Hello");
}
}
運行異常堆棧:
Exception in thread "main" java.lang.ExceptionInInitializerError
Caused by: java.lang.ArithmeticException: / by zero
at debug.Test.(Test.java:3) // ← 異常定位在!
// Test.java
public class Test {
Test() {
int i = 100 / 0; // 構造器拋異常
}
public static void main(String[] args) {
new Test();
}
}
運行異常堆棧:
Exception in thread "main" java.lang.ArithmeticException: / by zero
at debug.Test.(Test.java:4) // ← 異常定位在!
at debug.Test.main(Test.java:7)
結論:JVM不調用構造器Test(),而是調用<init>;不調用靜態塊,而是調用<clinit>。構造器/靜態塊/字段初始化均被編譯器線性化進這兩個方法。
<clinit>(遞歸,Object無<clinit>)
public class Test {
static int x = 0;
static String s = "123";
static {
String s1 = "456";
if (1 == 1) throw new RuntimeException();
}
}
javap -c Test 關鍵片段:
static {};
Code:
0: iconst_0
1: putstatic #x // x = 0
4: ldc "123"
6: putstatic #s // s = "123"
9: ldc "456"
11: astore_0 // s1 = "456"
12: new RuntimeException
15: dup
16: invokespecial ...
19: athrow // 拋異常
完全對應源碼順序。接口也有<clinit>:非常量static final字段(如Math.random()*100)需運行時計算,由接口<clinit>初始化。
super(...) / this(...)(必須首行,隱式super())
public class Test {
int x = 0;
String s = "123";
{ String s1 = "456"; }
public Test() { String ss = "789"; }
}
javap -c Test 關鍵片段:
public Test();
Code:
0: aload_0
1: invokespecial #Object. // super()
4: aload_0
5: iconst_0
6: putfield #x // x = 0
9: aload_0
10: ldc "123"
12: putfield #s // s = "123"
15: ldc "456"
17: astore_1 // s1 = "456"
18: ldc "789"
20: astore_1 // ss = "789"
21: return
順序:super() → x → s → {塊} → 構造器體。
this(...)調用另一構造器 → 目標構造器的<init>完整執行(含super、字段、塊、體),返回後當前<init>直接跳到return,不再執行字段/塊/體,避免重複初始化。
public class Test {
String s1 = rt("s1");
String s2 = "s2";
public Test() { s1 = "s1"; }
public Test(String s) { this(); if (1==1) throw new RuntimeException(); }
String rt(String s) { return s; }
}
javap -c Test:
public Test();
Code: ... // 完整初始化:super → s1 → s2 → s1="s1" → return
public Test(String);
Code:
0: aload_0
1: invokespecial #Test. // this() → 完整執行無參
4: new RuntimeException
7: dup
8: invokespecial ...
11: athrow
// 無字段/塊初始化!
super → 字段 → 塊 → 體。
public class Test2 extends Test1 {
static { System.out.print("3"); }
{ System.out.print("4"); }
Test2() { System.out.print("2"); }
public static void main(String[] args) { new Test2(); }
}
class Test1 {
static { System.out.print("6"); }
Test1() { System.out.print("5"); }
}
執行流:
Test2 → 觸發Test1. → print("6")Test2. → print("3")main → new Test2()Test2. → Test1. → print("5")Test2. → 字段/塊 → print("4")print("2")輸出:63542
若無new Test2():僅加載類 → 63(主類初始化觸發)
| 維度 | <clinit> (類初始化) | <init> (實例初始化) |
|---|---|---|
| 觸發時機 | 類首次主動使用(new、static字段/方法、反射、子類初始化、主類啟動) | 每次new、反射newInstance、反序列化、克隆 |
| 執行線程 | 單線程(JVM鎖保證),多線程並發初始化只有一個執行,其它阻塞等待 | 創建對象的線程 |
| 執行次數 | 每類加載器一次 | 每對象一次 |
| 內容 | 靜態字段賦值 + 靜態塊(源碼順序) | super/this → 實例字段賦值 + 實例塊(源碼順序) → 構造器體 |
| 異常處理 | 拋ExceptionInInitializerError包裝原異常,類標記為錯誤狀態,後續使用拋NoClassDefFoundError | 直接拋原異常,對象創建失敗 |
| 字節碼名 | <clinit> (static {}) | <init> (參數簽名對應重載) |
Class.forName觸發另一類初始化,雙向等待)static final int X = 1;編譯期常量 → 無putstatic指令,直接內聯,無<clinit>開銷Freeze動作(JMM),確保可見性ClassA靜態塊new ClassB(),ClassB靜態塊ClassA.field → 死鎖/錯誤值@PostConstruct在構造器<init>完成後、依賴注入後調用,天然安全<clinit>與<init>是JVM初始化的唯一兩個入口。編譯器將分散的聲明、塊、構造器線性化、扁平化為這兩個方法,嚴格遵循父先子後、源碼順序、super/this優先三大鐵律。理解字節碼級初始化機制,是排查ExceptionInInitializerError、NoClassDefFoundError、靜態字段異常值、構造器異常、死鎖、框架啟動順序等疑難雜症的核心鑰匙。