不可逆的類初始化過程
類的載入過程說複雜很複雜,說簡單也簡單,說複雜是因為細節很多,比如說今天要說的這個,可能很多人都不瞭解;說簡單,大致都知道類載入有這麼幾個階段,loaded->linked->initialized,為了讓大家能更輕鬆地知道我今天說的這個話題,我不詳細說類載入的整個過程,改天有時間有精力了我將整個類載入的過程和大家好好說說(PS:我對類載入過程慢慢清晰起來得益於當初在支付寶做cloudengine容器開發的時候,當時引入了標準的osgi,解決類載入的問題幾乎是每天的家常便飯,相信大家如果還在使用OSGI,那估計能體會我當時的那種痛,哈哈)。
本文我想說的是最後一個階段,類的初始化,但是也不細說其中的過程,只圍繞我們今天要說的展開。
我們定義一個類的時候,可能有靜態變數,可能有靜態程式碼塊,這些邏輯編譯之後會封裝到一個叫做clinit的方法裡,比如下面的程式碼:
class BadClass{ private static int a=100; static{ System.out.println("before init"); int b=3/0; System.out.println("after init"); } public static void doSomething(){ System.out.println("do somthing"); } }
編譯之後我們通過javap -verbose BadClass
可以看到如下位元組碼:
{ BadClass(); flags: Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 1: 0 public static void doSomething(); flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=0, args_size=0 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3 // String do somthing 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 10: 0 line 11: 8 static {}; flags: ACC_STATIC Code: stack=2, locals=1, args_size=0 0: bipush 100 2: putstatic #5 // Field a:I 5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 8: ldc #6 // String before init 10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 13: iconst_3 14: iconst_0 15: idiv 16: istore_0 17: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 20: ldc #7 // String after init 22: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 25: return LineNumberTable: line 2: 0 line 4: 5 line 5: 13 line 6: 17 line 7: 25 }
我們看到最後那個方法static{}
,其實就是我上面說的clinit方法,我們看到靜態欄位的初始化和靜態程式碼庫都封裝在這個方法裡。
假如我們通過如下程式碼來測試上面的類:
public static void main(String args[]){
try{
BadClass.doSomething();
}catch (Throwable e){
e.printStackTrace();
}
BadClass.doSomething();
}
大家覺得輸出會是什麼?是會列印多次before init
嗎?其實不然,輸出結果如下:
before init
java.lang.ExceptionInInitializerError
at ObjectTest.main(ObjectTest.java:7)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)
Caused by: java.lang.ArithmeticException: / by zero
at BadClass.<clinit>(ObjectTest.java:25)
... 6 more
Exception in thread "main" java.lang.NoClassDefFoundError: Could not initialize class BadClass
at ObjectTest.main(ObjectTest.java:12)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)
也就是說其實是隻輸出了一次before init
,這是為什麼呢?
clinit方法在我們第一次主動使用這個類的時候會觸發執行,比如我們訪問這個類的靜態方法或者靜態欄位就會觸發執行clinit,但是這個過程是不可逆的,也就是說當我們執行一遍之後再也不會執行了,如果在執行這個方法過程中出現了異常沒有被捕獲,那這個類將永遠不可用,雖然我們上面執行BadClass.doSomething()
的時候catch住了異常,但是當代碼跑到這裡的時候,在jvm裡已經將這個類打上標記了,說這個類初始化失敗了,下次再初始化的時候就會直接返回並丟擲類似的異常java.lang.NoClassDefFoundError: Could not initialize class BadClass
,而不去再次執行初始化的邏輯,具體可以看下jvm裡對類的狀態定義:
enum ClassState {
unparsable_by_gc = 0, // object is not yet parsable by gc. Value of _init_state at object allocation.
allocated, // allocated (but not yet linked)
loaded, // loaded and inserted in class hierarchy (but not linked yet)
linked, // successfully linked/verified (but not initialized yet)
being_initialized, // currently running class initializer
fully_initialized, // initialized (successfull final state)
initialization_error // error happened during initialization
};
如果clinit執行失敗了,拋了一個未被捕獲的異常,那將這個類的狀態設定為initialization_error
,並且無法再恢復,因為jvm會認為你這次初始化失敗了,下次肯定也是失敗的,為了防止不斷拋這種異常,所以做了一個快取處理,不是每次都再去執行clinit,因此大家要特別注意,類的初始化過程可千萬不能出錯,出錯就可能只能重啟了哦。