1. 程式人生 > >類載入的過程——解析。

類載入的過程——解析。

        解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程,在Class檔案中他以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等型別的常量飆戲那,那解析階段中所說的直接引用與符號引用又有什麼關聯呢?

  • 符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義的定位到目標即可。符號引用與虛擬機器實現的記憶體佈局無關,引用的目標並不一定已經載入到記憶體中。各種虛擬機器實現的記憶體佈局可以各不相同,但是他們能接受的符號引用必須都是一致的,因為符號引用的字面量形式明確定義在Java虛擬機器規範的Class檔案格式中。
  • 直接引用(Direct References):直接引用可以是直接指向目標的指標、相對偏移量或是一個能簡介定位到目標的控制代碼。直接引用是和虛擬機器實現的記憶體佈局相關的,同一個符號引用在不同的虛擬機器例項上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經在記憶體中存在。

        虛擬機器規範之中並未規定解析階段發生的具體實現,只要求了在執行anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield和putstatic這16個用於操作符號引用的位元組碼指令之前,先對他們所使用的符號引用進行解析。所以虛擬機器實現可以根據需要來判斷到底是在類被載入器載入時就對常量池中的符號引用進行解析,還是等到一個符號引用將要被使用前採取解析他。

        對同一個符號引用進行多次解析請求是很常見的事情,除invokedynamic指令以外,虛擬機器實現可以對第一次解析的結果進行快取(在執行時常量池中記錄直接引用,並把常量標識為已解析狀態)從而避免解析動作重複進行。無論是否真正執行了多次解析動作,虛擬機器需要保證的是在同一個實體中,如果一個符號引用之前已經被成功解析過,那麼後續的引用解析請求就應當一直成功;同樣的,如果第一次解析失敗了,那麼其他指令對這個符號的解析請求也應該受到相同的異常。

        對於invokedynamic指令,上面規則則不成立。當碰到某個前面已經由invokedynamic指令觸發過解析的符號引用時,並不意味著這個解析結果對於其他invokedynamic指令也同樣生效。因為invokedynamic指令的目的本來就是用於動態語言支援(目前僅使用Java語言不會生成這條位元組碼指令),他所對應的引用稱為“動態呼叫點限定符”(Dynamic Call Site Specifier),這裡“動態”的含義就是必須等到程式實際執行到這條指令的時候,解析動作才能進行。相對的,其餘可觸發解析的指令都是“靜態”的,可以在剛剛完成載入階段,還沒有開始執行程式碼時就進行解析。

        解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制代碼和呼叫點限定符7類符號引用進行,分別對應於常量池的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info 7種常量型別。下面將講解前面4種引用的解析過程。

類或介面的解析

        假設當前程式碼所處的類為D,如果要把一個從未解析過的符號引用N解析為一個類或介面C的直接引用,那虛擬機器完成整個解析的過程需要以下3個步驟:

  1. 如果C不是一個數組型別,那虛擬機器將會把代表N的全限定名傳遞給D的類載入器去載入這個類C。在載入過程中,由於元資料驗證、位元組碼驗證的需要,又可能觸發其他相關類的載入動作,例如載入這個類的父類或實現的介面。一旦這個載入過程出現了任何異常,解析過程就宣告失敗。
  2. 如果C是一個數組型別,並且陣列的元素型別為物件,也就是N的描述符會是類似“[Ljava/lang/Integer”的形式,那將會按照第1點的規則載入陣列元素型別。如果N的描述符如前面所假設的形式,需要載入的元素型別就是“java.lang.Integer”,接著由虛擬機器生成一個代表此陣列維度和元素的陣列物件。
  3. 如果上面的步驟沒有出現任何異常,那麼C在虛擬機器中實際上已經成為一個有效的類或介面了,但在解析完成之前還要進行符號引用驗證,確認D是否具備對C的訪問許可權。如果發現不具備訪問許可權,將丟擲java.lang.IllegalAccessError異常。

欄位解析

        要解析一個未被解析過的欄位符號引用,首先將會對欄位表內class_index項中索引的CONSTANT_Class_info符號引用進行解析,也就是欄位所屬的類或介面的符號引用。如果在解析這個類或介面符號引用的過程中出現了任何異常,都會導致欄位符號引用解析的失敗。如果解析成功完成,那將這個欄位所屬的類或介面用C表示,虛擬機器規範要求按照如下步驟對C進行後續欄位的搜尋。

  1. 如果C本身就包含了簡單名稱和欄位描述符都與目標相匹配的欄位,則返回這個欄位的直接引用,查詢結束。
  2. 否則,如果在C中實現了介面,將會按照繼承關係從下往上遞迴搜尋各個介面和他的父介面,如果介面中包含了簡單名稱和欄位描述符都與目標相匹配的欄位,則返回這個欄位的直接引用,查詢結束。
  3. 否則,如果C不是java.lang.Object的話,將會按照繼承關係從下往上遞迴搜尋其父類,如果在父類中包含了簡單名稱和欄位描述符都與目標相匹配的欄位,則返回這個欄位直接引用,查詢失敗。
  4. 否則,查詢失敗,丟擲java.lang.NoSuchFieldError異常。

        如果查詢過程成功返回了引用,將會對這個欄位進行許可權驗證,如果發現不具備對欄位的訪問許可權,將丟擲java.lang.IllegalAccessError異常。

        在實際應用中,虛擬機器的編譯器實現可能會比上述規範要求的更加嚴格一些,如果有一個同名欄位同時出現在C的介面和父類中,或者同時在自己或父類的多個介面中出現,那編譯器將可能拒絕編譯。在下面程式碼示例中,如果註釋了Sub類中的“public static int A=4; ”,介面與父類同時存在欄位A,那編譯器將提示“The field Sub.A is ambiguous”,並且拒絕編譯這段程式碼。

public class FieldResolution {

    interface Interface0 {
        int A = 0;
    }

    interface Interface1 extends Interface0 {
        int A = 1;
    }

    interface Interface2 {
        int A = 2;
    }

    static class Parent implements Interface1 {
        public static int A = 3;
    }

    static class Sub extends Parent implements Interface2 {
        public static int A = 4;
    }

    public static void main(String[] args) {
        System.out.println(Sub.A);
    }
}

類方法解析

        類方法解析的第一個步驟與欄位解析一樣,也需要先解析出類方法表的class_index項中索引的方法所屬的類或介面的符號引用,如果解析成功,我們依然用C表示這個類,接下來虛擬機器將會按照如下步驟進行後續的類方法搜尋。

  1. 類方法和介面方法符號引用的常量型別定義是分開的,如果在類方法表中發現class_index中索引的C是個介面,那就直接丟擲java.lang.IncompatibleClassChangeError異常。
  2. 如果通過了第1步,在類C中查詢是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查詢結束。
  3. 否則,在類C的父類中遞迴查詢是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查詢結束。
  4. 否則,在類C實現的介面列表及他們的父介面之中遞迴查詢是否有簡單名稱和描述符都與目標相匹配的方法,如果存在匹配的方法,說明類C是一個抽象,這時查詢結束,丟擲java.lang.AbstractMethodError異常。
  5. 否則,宣告方法查詢失敗,丟擲java.lang.NoSuchMethodError。

        最後,如果查詢過程成功返回了直接引用,將會對這個方法進行許可權驗證,如果發現不具備對此方法的訪問許可權,將丟擲java.lang.IllegalAccessError異常。

介面方法解析

        介面方法也需要先解析出介面方法表的class_index項中索引的方法所屬的類或介面的符號引用,如果解析成功,依然用C表示這個介面,接下來虛擬機器將會按照如下步驟進行後續的介面方法搜尋。

  1. 與類方法解析不同,如果在介面方法表中發現class_index中的索引C是個類而不是介面,那就直接丟擲java.lang.IncompatibleClassChangeError異常。
  2. 否則,在介面C中查詢是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查詢結束。
  3. 否則,在介面C的父介面中遞迴查詢,直到java.lang.Object(查詢範圍會包括Object類)為止,看是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查詢結束。
  4. 否則,宣告方法查詢失敗,丟擲java.lang.NoSuchMethodError異常。
  5. 由於介面中的所有方法預設都是public,所以不存在訪問許可權的問題,因此介面方法的符號解析應當不會丟擲java.lang.IllegalAccessError異常。