首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

JVM初始化機制深度解析:<clinit>與<init>的字節碼視角

     編輯:Java 相關知識

前言:拒絕「可以這樣認為」,直面JVM真相

許多教材用「仮想Initialize()方法」模糊描述初始化,實則JVM有兩個真實存在、字節碼可見、不可直接調用的特殊方法:<clinit>(類初始化)與<init>(實例初始化)。本文通過javap -c反編譯、異常堆棧定位、JLS §12.4/§12.5規範,徹底還原初始化全貌。

一、 兩大初始化方法證據

1. <clinit>:類/接口初始化


// 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)  // ← 異常定位在!

2. <init>:實例初始化


// 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> 線性化規則

執行順序(JLS §12.4.2)

  1. 父類<clinit>(遞歸,Object無<clinit>)
  2. 本類靜態字段顯式初始化 + 靜態初始化塊,**按源碼順序**交織執行

驗證案例


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>初始化。

三、 <init> 線性化規則

執行順序(JLS §12.5)

  1. super(...) / this(...)(必須首行,隱式super())
  2. 本類實例字段顯式初始化 + 實例初始化塊,**按源碼順序**交織執行
  3. 本類構造器體代碼

驗證案例


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()調用

規則

: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
     // 無字段/塊初始化!

無this()調用的獨立構造器

:各自完整執行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"); }
}

執行流:

  1. 加載Test2 → 觸發Test1. → print("6")
  2. Test2. → print("3")
  3. main → new Test2()
  4. Test2. → Test1. → print("5")
  5. 返回Test2. → 字段/塊 → print("4")
  6. 構造器體 → print("2")

輸出:63542

若無new Test2():僅加載類 → 63(主類初始化觸發)

六、 核心差異總結表

維度<clinit> (類初始化)<init> (實例初始化)
觸發時機類首次主動使用(new、static字段/方法、反射、子類初始化、主類啟動)每次new、反射newInstance、反序列化、克隆
執行線程單線程(JVM鎖保證),多線程並發初始化只有一個執行,其它阻塞等待創建對象的線程
執行次數每類加載器一次每對象一次
內容靜態字段賦值 + 靜態塊(源碼順序)super/this → 實例字段賦值 + 實例塊(源碼順序) → 構造器體
異常處理拋ExceptionInInitializerError包裝原異常,類標記為錯誤狀態,後續使用拋NoClassDefFoundError直接拋原異常,對象創建失敗
字節碼名<clinit> (static {})<init> (參數簽名對應重載)

七、 實戰避坑指南

  1. 靜態塊避免耗時/阻塞/死鎖操作:類初始化鎖住Class對象,易觸發死鎖(如靜態塊調用Class.forName觸發另一類初始化,雙向等待)
  2. final靜態字段常量折疊:static final int X = 1;編譯期常量 → 無putstatic指令,直接內聯,無<clinit>開銷
  3. 實例字段final語義:構造器末尾有Freeze動作(JMM),確保可見性
  4. 循環依賴檢測:ClassA靜態塊new ClassB(),ClassB靜態塊ClassA.field → 死鎖/錯誤值
  5. 框架初始化順序:Spring@PostConstruct在構造器<init>完成後、依賴注入後調用,天然安全

總結

<clinit>與<init>是JVM初始化的唯一兩個入口。編譯器將分散的聲明、塊、構造器線性化、扁平化為這兩個方法,嚴格遵循父先子後、源碼順序、super/this優先三大鐵律。理解字節碼級初始化機制,是排查ExceptionInInitializerError、NoClassDefFoundError、靜態字段異常值、構造器異常、死鎖、框架啟動順序等疑難雜症的核心鑰匙。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved