java類載入之初始化過程(附面試題)
類或介面的初始化過程就是執行它們的初始化方法
<clinit>
。這個方法是由編譯器在編譯的時候生成到class檔案中的,包含類靜態field賦值指令和靜態語句塊(static{})中的程式碼指令兩部分,順序和原始碼中的順序相同。
以下情況下,會觸發類(用C表示)的初始化:
-
new(建立物件),getstatic(獲取類field),putstatic(給類field賦值),或 invokestatic(呼叫類方法) 指令執行,建立C的例項,獲取/設定C的靜態欄位,呼叫C的靜態方法。
如果獲取的類field是帶有ConstantValue屬性的常量,不會觸發初始化
-
第一次呼叫
java.lang.invoke.MethodHandle
REF_getStatic
,REF_putStatic
,REF_invokeStatic
,REF_newInvokeSpecial
型別的方法控制程式碼。 -
反射呼叫,如Class
或
java.lang.reflect`包中的類 -
如果C是一個類,它的子類
<clinit>
方法呼叫前,先呼叫C的<clinit>
方法 -
如果C是一個介面,並且定義了一個非
abstract
,非static
的方法,它的實現類(直接或間接)執行初始化方法<clinit>
時會先初始化C. -
C作為主類(包含main方法)時
可以看出在static{}中執行一些耗時的操作會導致類初始化阻塞甚至失敗
在類初始化之前,會先進行連結操作
為了加快初始化效率,jvm是多執行緒執行初始化操作的,可能會有多個執行緒同一時刻嘗試初始化類,也可能一個類初始化過程中又觸發遞迴初始化該類,所以jvm需要保證只有一個執行緒去進行初始化動作,jvm通過為已驗證過的類保持一個狀態和一個互斥鎖來保證初始化過程是執行緒安全的。
虛擬機器器中類的狀態:
- 類已驗證和準備,但未初始化
- 類正在被一個執行緒初始化
- 類已經完成初始化,可以使用了
- 類初始化失敗
實際上,虛擬機器器為類定義的狀態可能不止上面4種,如hotspot,見前文
除了狀態,在初始化一個類之前,先要獲得與這個類相關聯的鎖物件(監視器),記作LC。
類或介面C的初始化流程如下(jvm1.8規範):
-
等待獲取C的鎖LC.
-
如果C正在被其他執行緒初始化,釋放LC,並阻塞當前執行緒直到C初始化完成.
執行緒中斷對初始化過程沒有影響
-
如果C正在被當前執行緒初始化,則肯定是在遞迴初始化時又觸發C初始化. 釋放LC並正常返回.
-
如果C的狀態為已經初始化,釋放LC並正常返回.
-
如果C的狀態為初始化失敗,釋放LC並丟擲一個
NoClassDefFoundError
異常. -
否則記錄當前類C的狀態為初始化中,並設定當前執行緒為初始化執行緒,然後釋放LC.
然後,按照位元組碼檔案中的順序初始化C中每個帶有
ConstantValue
屬性的final
static
欄位.**注意:**jvm規範把常量的賦值定義在初始化階段,
<clinit>
執行之前,具體實現未必嚴格遵守。如hotspot虛擬機器器在解析位元組碼過程建立_java_mirror
映象類時已為每個常量欄位賦值。 -
下一步,如果C是一個類,而且它的父類還未初始化,SC記作它的父類,
SI1,...,SIn
記作C實現的至少包含一個非抽象,非靜態方法的介面(直接或間接的) 。 先初始化SC,所有父介面的順序按照遞迴的順序而不是繼承層次的順序確定, 對於一個被C直接實現的介面I (按照C的介面列表interfaces
的順序),在I初始化之前,先迴圈遍歷初始化I的父介面 (按照I的介面列表interfaces
的順序) . -
下一步,檢視定義類載入器是否開啟了斷言(用於除錯).
// ClassLoader // 查詢類是否開啟了斷言 // 通過#setClassAssertionStatus(String,boolean)/#setPackageAssertionStatus(String,boolean)/#setDefaultAssertionStatus(boolean)設定斷言 boolean desiredAssertionStatus(String className); 複製程式碼
-
下一步,執行C的初始化方法
<clinit>
. -
如果C的初始化正常完成,獲取LC並將C的狀態標記為已完成初始化,喚醒所有等待執行緒,釋放鎖LC,初始化過程完成.
-
否則,初始化方法必須丟擲一個異常E. 如果E不是
Error
或其子類,建立一個ExceptionInInitializerError
例項(以E作為引數),在接下來的步驟中,以這個例項替換E,如果因為記憶體溢位無法建立ExceptionInInitializerError
例項,用一個OutOfMemoryError
替換E. -
獲取
LC
,標記C的初始化狀態為發生錯誤,通知所有等待執行緒,釋放LC
,並通過E或其他替代(見前一步)異常返回.
虛擬機器器的實現可能優化這個過程,在它可以判斷初始化已經完成時, 取消在第1步獲取鎖 (和在第 4/5釋放鎖),前提是,根據java記憶體模型,所有的 happens-before 關係在加鎖和優化鎖時都存在.
接下來看一個例子:
interface IA {
Object o = new Object();
}
abstract class Base {
static {
System.out.println("Base <clinit> invoked");
}
public Base() {
System.out.println("Base <init> invoked");
}
{
System.out.println("Base normal block invoked");
}
}
class Sub extends Base implements IA {
static {
System.out.println("Sub <clinit> invoked");
}
{
System.out.println("Sub normal block invoked");
}
public Sub() {
System.out.println("Sub <init> invoked");
}
}
public class TestInitialization {
public static void main(String[] args) {
new Sub();
}
}
複製程式碼
在hotspot虛擬機器器上執行:
javac TestInitialization.java && java TestInitialization
複製程式碼
可以看出初始化順序為:父類靜態構造器 -> 子類靜態構造塊 -> 父類普通構造塊 -> 父類構造器 -> 子類普通構造快 -> 子類構造器
,且普通構造快在例項構造器之前呼叫,與順序無關。
關於介面由於沒法新增static{},可以通過反編譯看下也生成了<clinit>
方法:
如果沒有為類定義例項構造器,編譯器會生成一個不帶引數的預設構造器,裡邊呼叫父類的預設構造器
如果類中沒有靜態變數的賦值語句或靜態程式碼塊,則不必生成<clinit>
最後,介紹幾個相關面試題:
-
下面程式碼輸出什麼?
public class InitializationQuestion1 { private static InitializationQuestion1 q = new InitializationQuestion1(); private static int a; private static int b = 0; public InitializationQuestion1() { a++; b++; } public static void main(String[] args) { System.out.println(InitializationQuestion1.a); System.out.println(InitializationQuestion1.b); } } 複製程式碼
把q宣告放到b後面呢?輸出什麼?
-
下面程式碼輸出什麼?
abstract class Parent { static int a = 10; static { System.out.println("Parent init"); } } class Child extends Parent { static { System.out.println("Child init"); } } public class InitializationQuestion2 { public static void main(String[] args) { System.out.println(Child.a); } } 複製程式碼
改成下面試試:
abstract class Parent { static final int a = 10; static { System.out.println("Parent init"); } } 複製程式碼
再改成下面這樣試試:
abstract class Parent { static final int a = value(); static { System.out.println("Parent init"); } static int value(){ return 10; } } 複製程式碼