1. 程式人生 > >不可逆的類初始化過程

不可逆的類初始化過程

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