不可逆的類初始化過程
- 2020 年 4 月 9 日
- 筆記
類的載入過程說複雜很複雜,說簡單也簡單,說複雜是因為細節很多,比如說今天要說的這個,可能很多人都不了解;說簡單,大致都知道類載入有這麼幾個階段,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,因此大家要特別注意,類的初始化過程可千萬不能出錯,出錯就可能只能重啟了哦。類的載入過程說複雜很複雜,說簡單也簡單,說複雜是因為細節很多,比如說今天要說的這個,可能很多人都不了解;說簡單,大致都知道類載入有這麼幾個階段,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,因此大家要特別注意,類的初始化過程可千萬不能出錯,出錯就可能只能重啟了哦。
推薦閱讀