首頁>技術>

在接觸了類載入的基本知識以後,我們已經清楚了類載入大體分為3個階段:

載入連線初始化

今天我們主要講解類載入的第二個階段-連線階段。連線階段又可以分為三個部分:

驗證準備解析驗證

由於我們的位元組碼來源多樣化,並不一定來源於Class檔案,所以我們需要透過一些措施來保證位元組碼的二進位制流是正確的安全的,因此我們需要透過驗證來避免虛擬機器受到攻擊。透過驗證階段的位元組碼也並不是百分之百安全的。

驗證階段大體會有4個階段的驗證:

檔案格式驗證元資料格式驗證位元組碼驗證符號引用驗證檔案格式驗證

由於我們的位元組碼檔案來源多樣化,因此我們需要對其進行驗證,驗證的方向主要由以下幾個方面:

檔案是否以魔數開頭OxCAFEBABE主、次版本號是否在虛擬機器可以處理的範圍之內常量池中是否有不被支援的常量型別指向常量池中的各種索引值是否有指向不存在的常量或者不符合型別的常量CONSTANT_Utf8_info的常量中是否有不適合UTF8編碼的資料Class檔案中各個部分及檔案本身是否有被刪除或附加的其他資訊

檔案格式驗證是唯一根據位元組碼二進位制流進行驗證的階段,當檔案格式階段驗證透過以後,位元組碼二進位制流會進入記憶體的方法區(元資料區)進行儲存。所以後面的3個驗證階段都是基於方法區(元資料區)的結構進行驗證的。

元資料格式驗證

元資料格式驗證主要是對位元組碼描述的資訊進行語義分析,保證其描述的資訊符合Java語言的規範,主要包含以下幾個方面的驗證:

是否有父類(除了java.lang.Object,所有的類都有父類)是否繼承了不允許被繼承的類(final修飾的)如果這個類不是抽象類,是否實現了其父類或介面要求必須實現的所有方法類中的欄位、方法是否與父類產生矛盾(例如覆蓋父類的final欄位或者出現不合規則的重寫及過載)位元組碼驗證

位元組碼驗證主要是對類的方法體進行校驗分析,保證方法在執行時不會做出危害虛擬機器的事情:

保證任意時刻運算元棧的資料型別與指令程式碼都能配合工作,不能出現採用long型別的載入指令將int型別的運算元棧元素儲存到區域性變量表等類似的情況保證跳轉指令不會跳到方法體以外的位元組碼指令上保證方法體中的型別轉換是有效的

位元組碼驗證的流程相對複雜,在JDK1.6之前都是採用基於資料流進行推導驗證,為了減少該階段的效能消耗,JDK1.6以後在Code屬性的屬性表上增加了StackMapTable屬性,該屬性描述了方法體中所有基本塊(按照控制流拆分的程式碼塊)開始時本地變量表和運算元棧應有的狀態,位元組碼驗證期間就不需要根據程式進行推導,而是直接檢查StackMapTable屬性中的記錄是否合法。

理論上StackMapTable屬性存在錯誤和被篡改的可能,如果同時修改Code屬性和StackMapTable屬性可以繞過虛擬機器的型別校驗,因此沒有透過驗證的位元組碼肯定是有問題的,但是透過驗證的位元組碼也不是百分之百安全的。

JDK1.7,主版本號大於50的Class檔案,使用StackMapTable進行分析校驗是唯一的選擇,不允許根據資料流進行推導。

符號引用驗證

符號引用驗證階段通常發生在虛擬機器將符號引用轉換為直接引用的過程,這個過程將在連線的第三階段解析階段發生。

符號引用驗證是對類自身以外的常量池中的各種符號引用進行匹配校驗:

符號引用中透過字串描述的全限定名能否找到對應的類符號引用中的類中是否存在符合方法的欄位描述符以及簡單名稱所描述的方法和欄位符號引用中的類、欄位、方法的訪問性是否可以被當前類訪問

符號驗證如果無法透過,將會丟擲java.lang.IncompatibleClassChangeError異常的子類,如java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。

準備

準備階段是為類變數(static)設定記憶體並分配初始值的階段,這裡強調以下兩點:

只是類變數,不包含例項變數,例項變數會在物件例項化的時候分配到堆上,但類變數(變數記憶體)都會在方法區(元資料)中分配記憶體。只是分配初始值,初始值見下圖,有一種情況例外,就是如果欄位屬性表有ConstantValue(stati final修飾的變數)屬性,準備階段就會為變數賦值而不是初始值

[email protected]

這裡我們來簡單說一下變數分配,Java中的變數按其引用型別可以劃分為原始型別,和引用型別。變數記憶體的佔用其實有兩部分,一部分是變數的記憶體佔用,還有一部分是變數所指向的資料佔用的記憶體,分別稱為變數記憶體和資料記憶體。

原始型別的變數記憶體和資料記憶體往往是分配在同一區域,但引用型別的變數記憶體和資料記憶體是不一定位於相同的區域的。

解析

解析階段是虛擬機器將常量池中的符號引用替換為直接引用的過程,符號引用在Class檔案中以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等型別的常量出現。

符號引用:以一組符號表示引用的目標,可以是任何形式的字面量,只要可以定位到目標即可。

直接引用:直接指向目標的指標、相對偏移量或是一個能間接定位到目標的控制代碼。

如果有了直接引用,那麼引用的目標必定已經在記憶體中存在。

虛擬機器要求在執行以下16個命令之前必須對所使用的符號引用進行解析:

anewarraycheckcastgetfieldgetstaticinstanceofinvokedynamicinvokeinterfaceinvokespecialinvokestaticinvokevirtualldcldc_wmultianewarraynewputfiledputstatic

除了使用invokedynamic指令,虛擬機器可以對符號引用的結果可以進行快取(在執行時常量池記錄直接引用),避免解析動作重複進行。無論是否執行了多次解析,虛擬機器需要保證在同一個實體中,如果一個符號引用曾經被成功解析,那麼後續的解析也必須成功,如果失敗,後續的其他指令對該符號引用的解析請求也必須相應的失敗。

解析動作主要針對類、介面、欄位、類方法、介面方法、方法型別、方法控制代碼和呼叫點限定符,分別對應常量池的:

CONSTANT_Class_infoCONSTANT_Fieldref_infoCONSTANT_Methodref_infoCONSTANT_InterfaceMethodref_infoCONSTANT_MethodType_infoCONSTANT_MethodHandle_infoCONSTANT_InvokeDynamic_info類和介面的解析

我們假設我們所處的類為A,要把一個從未解析的符號引用M解析為一個類或者介面B的直接引用,步驟如下:

如果B不是一個數組型別,那麼虛擬機器會把代表M的全限定名傳遞給A的類載入器去進行類載入B。如果在類載入B的過程發生異常,則解析過程失敗如果B是一個數組型別,將會按照第1點去載入陣列元素型別中的類,接著由虛擬機器生成一個代表此陣列緯度和元素的陣列物件如果前兩個步驟通過了,那麼B在虛擬機器中已經成為一個有效的類或者介面了,最後進行符號引用驗證(驗證階段的第4個步驟),確認A是否有對C的訪問許可權。如果沒有許可權訪問,丟擲java.lang.IllegalAccessError異常欄位的解析

解析一個未被解析過的欄位的符號引用時,首先要對其CONSTANT_Class_info進行解析。如果解析失敗,則欄位的符號引用解析失敗。解析成功以後,這裡假設類B被成功解析,接著會對B的欄位進行解析:

如果B本身就包含了簡單名稱和欄位描述都匹配的欄位,則返回這個欄位的直接引用,結束否則,如果C實現了介面,將會按照繼承關係從下往上遞迴搜尋各個介面或者它的父介面,如果找到了匹配的欄位,返回直接引用,查詢結束否則,如果C不是java.lang.Object,將按照繼承關係從下往上遞迴搜尋父類,如果找到了匹配的欄位,返回直接直接引用否則查詢失敗,丟擲java.lang.NoSuchFieldError異常在返回直接引用以前,會對這個欄位做許可權校驗,如果發現A不具備這個欄位的訪問許可權,那麼丟擲java.lang.IllegalAccessError異常類方法解析

解析一個未被解析過的方法的符號引用時,首先要對其CONSTANT_Class_info進行解析。如果解析失敗,則方法的符號引用解析失敗。解析成功以後,這裡假設類B被成功解析,接著會對B的方法進行解析:

如果發現B是一個介面,解析失敗,丟擲java.lang.IncompatibleClassChangeError確認B是一個類以後,在類B中查詢是否有簡單名稱和方法描述符都相匹配的方法,如果有,返回這個方法的直接引用,查詢結束否則,在B的父類中遞迴查詢是否有匹配的方法,如果有則返回這個方法的直接引用,查詢結束否則,在B實現的介面列表和它們的父介面中遞迴查詢是否有匹配的方法,如果有匹配的方法,說明B是一個抽象類,丟擲java.lang.AbstractMethodError異常否則,查詢失敗,丟擲java.lang.NoSuchMethodError在返回直接引用以前,需要對這個方法許可權校驗,如果發現A不具備對這個方法的訪問許可權,那麼丟擲java.lang.IllegalAccessError異常介面方法解析

解析一個未被解析過的介面方法的符號引用時,首先要對其CONSTANT_Class_info進行解析。如果解析失敗,則介面方法的符號引用解析失敗。解析成功以後,這裡假介面B被成功解析,接著會對B的方法進行解析:

如果B是個類不是介面,解析失敗,丟擲java.lang.IncompatibleClassChangeError否則,在介面B中遞迴查詢是否有匹配的方法,如果有則返回這個方法的直接引用,查詢結束否則,在介面B的父介面中遞迴查詢,直到java.lang.Object為止,如果找到匹配的方法,則返回這個方法的直接引用,查詢結束否則,方法查詢失敗,丟擲java.lang.NoSuchMethodError異常

介面方法不會對許可權進行校驗,因為介面方法預設是public。

本期類載入的連線階段就介紹到這,下期我們會講解類載入的初始化階段,我們下期再見!!!

11
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 初學者快速理解Java面向物件思想