不可逆的類初始化過程

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

推薦閱讀

這麼流行的ZooKeeper,原來是這樣設計的!

spring boot 引起的 “堆外內存泄漏”