java類加載之初始化過程(附面試題)

類或接口的初始化過程就是執行它們的初始化方法<clinit>。這個方法是由編譯器在編譯的時候生成到class文件中的,包含類靜態field賦值指令和靜態語句塊(static{})中的代碼指令兩部分,順序和源碼中的順序相同。java

如下狀況下,會觸發類(用C表示)的初始化:面試

  • new(建立對象), getstatic(獲取類field), putstatic(給類field賦值), 或 invokestatic(調用類方法) 指令執行,建立C的實例,獲取/設置C的靜態字段,調用C的靜態方法。安全

    若是獲取的類field是帶有ConstantValue屬性的常量,不會觸發初始化bash

  • 第一次調用 java.lang.invoke.MethodHandle 實例返回了REF_getStatic,REF_putStatic, REF_invokeStatic, REF_newInvokeSpecial類型的方法句柄。多線程

  • 反射調用,如Classjava.lang.reflect`包中的類app

  • 若是C是一個類,它的子類<clinit>方法調用前,先調用C的<clinit>方法jvm

  • 若是C是一個接口,而且定義了一個非abstract, 非static 的方法, 它的實現類(直接或間接)執行初始化方法<clinit>時會先初始化C.優化

  • C做爲主類(包含main方法)時spa

能夠看出在static{}中執行一些耗時的操做會致使類初始化阻塞甚至失敗線程

在類初始化以前,會先進行連接操做

爲了加快初始化效率,jvm是多線程執行初始化操做的,可能會有多個線程同一時刻嘗試初始化類,也可能一個類初始化過程當中又觸發遞歸初始化該類,因此jvm須要保證只有一個線程去進行初始化動做,jvm經過爲已驗證過的類保持一個狀態和一個互斥鎖來保證初始化過程是線程安全的。

虛擬機中類的狀態:

  • 類已驗證和準備,但未初始化
  • 類正在被一個線程初始化
  • 類已經完成初始化,可使用了
  • 類初始化失敗

實際上,虛擬機爲類定義的狀態可能不止上面4種,如hotspot,見前文

除了狀態,在初始化一個類以前,先要得到與這個類相關聯的鎖對象(監視器),記做LC。

類或接口C的初始化流程以下(jvm1.8規範):

  1. 等待獲取C的鎖LC.

  2. 若是C正在被其餘線程初始化, 釋放LC,並阻塞當前線程直到C初始化完成.

    線程中斷對初始化過程沒有影響

  3. 若是C正在被當前線程初始化, 則確定是在遞歸初始化時又觸發C初始化. 釋放LC並正常返回.

  4. 若是C的狀態爲已經初始化,釋放LC並正常返回.

  5. 若是C的狀態爲初始化失敗,釋放LC並拋出一個 NoClassDefFoundError異常.

  6. 不然記錄當前類C的狀態爲初始化中,並設置當前線程爲初始化線程, 而後釋放LC.

    而後, 按照字節碼文件中的順序初始化C中每一個帶有ConstantValue屬性的 final static 字段.

    **注意:**jvm規範把常量的賦值定義在初始化階段,<clinit>執行以前,具體實現未必嚴格遵照。如hotspot虛擬機在解析字節碼過程建立_java_mirror鏡像類時已爲每一個常量字段賦值。

  7. 下一步, 若是C是一個類, 並且它的父類還未初始化, SC記做它的父類, SI1, ..., SIn 記做C實現的至少包含一個非抽象,非靜態方法的接口(直接或間接的) 。 先初始化SC,全部父接口的順序按照遞歸的順序而不是繼承層次的順序肯定, 對於一個被C直接實現的接口I (按照C的接口列表 interfaces 的順序), 在I初始化以前,先循環遍歷初始化I的父接口 (按照I的接口列表 interfaces 的順序) .

  8. 下一步, 查看定義類加載器是否開啓了斷言(用於調試).

    // ClassLoader
    
    // 查詢類是否開啓了斷言
    // 經過#setClassAssertionStatus(String, boolean)/#setPackageAssertionStatus(String, boolean)/#setDefaultAssertionStatus(boolean)設置斷言
    boolean desiredAssertionStatus(String className);
    複製代碼
  9. 下一步,執行C的初始化方法<clinit>.

  10. 若是C的初始化正常完成, 獲取LC並將C的狀態標記爲已完成初始化, 喚醒全部等待線程,釋放鎖LC,初始化過程完成.

  11. 不然, 初始化方法必須拋出一個異常E. 若是E不是 Error 或其子類, 建立一個 ExceptionInInitializerError 實例(以E做爲參數), 在接下來的步驟中,以這個實例替換E,若是由於內存溢出沒法建立 ExceptionInInitializerError 實例,用一個 OutOfMemoryError 替換E.

  12. 獲取 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>

最後,介紹幾個相關面試題:

  1. 下面代碼輸出什麼?

    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後面呢?輸出什麼?

  2. 下面代碼輸出什麼?

    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;
        }
    }
    複製代碼

相關文章
相關標籤/搜索