1. 程式人生 > >深入理解JVM(七)——虛擬機器類載入機制

深入理解JVM(七)——虛擬機器類載入機制

虛擬機器類載入機制

虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗,轉換解析,初始化,最終形成可以被虛擬機器直接使用的Java型別,這就是Java的類載入機制。

類的載入,連線,初始化是在程式執行時完成的。

生命週期

載入—->連線(驗證->準備->解析)—->初始化—->使用—->解除安裝

載入,驗證,準備,初始化和解除安裝這5個步驟是確定的,類載入過程必須按照這個順序。而解析階段則不一定,某些情況下可以在初始化之後再開始,這是為了支援Java語言的動態繫結。

初始化的條件

Java虛擬機器規範規定遇到一下5種情況必須立即對類進行初始化。

  1. 遇到new,getstatic,putstatic或者invokestatic這4條字元碼指令時,如果類沒有進行過初始化,則必須先觸發其初始化。生成這4條指令常見的Java程式碼有:使用new關鍵字例項化物件的時候,讀取或設定一個類的靜態變數(被final修飾的在編譯期把結果放在常量池的靜態欄位除外),以及呼叫一個類的靜態方法時。
  2. 使用reflect包的方法對類進行反射呼叫時
  3. 當初始化一個類的時候發現其父類還沒有初始化時,則需先觸發其父類初始化
  4. 當虛擬機器啟動時,使用者需要指定一個要執行的主類,虛擬機器會先初始化這個類
  5. 當使用JDK1.7的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項的解析結果是REF_getstatic,REF_putstatic,REF_invokeStatic的方法控制代碼時,並且這個方法的類沒有進行過初始化。

    對於靜態欄位,只有直接定義這個欄位的類才會被初始化,因此通過其子類來引用父類中定義的靜態欄位,只會觸發父類的初始化而不會觸發子類的初始化。

載入

在載入的階段,虛擬機器完成以下3件事

  1. 通過一個類的全限定名來獲取定義此類的二進位制位元組流
  2. 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。
  3. 在記憶體中生成一個代表這個類的java.lang.class物件,作為方法區這個類的各種資料的訪問入口。

載入階段完成後,虛擬機器外部的二進位制位元組流就按照虛擬機器所需的格式儲存在方法區之中,方法區的資料儲存格式由虛擬機器實現自行定義。然後在記憶體中例項化一個java.lang.class物件(並沒有明確規定在Java堆中,對於HotSpot虛擬機器來說,Class物件比較特殊,存放在方法區)

驗證

驗證是連線階段的第一步,目的是確保Class檔案的位元組流中包含的資訊字元符合虛擬機器的要求,並且不會危害虛擬機器的安全。

如果輸入的位元組流不符合虛擬機器的規範,虛擬機器會丟擲java.lang.VerifyError異常或子類

驗證內容包括

  1. 檔案格式驗證
    是否以魔數0xCAFEBABE開頭
    主,次版本號是否在當前虛擬機器處理的範圍內
    常量池的常量是否有不被支援的常量型別
    CONSTANT_Utf8_info型的常量 中是否有不符合UTF8編碼的資料
    Class檔案中各部分及檔案本身是否被刪除或者附加其它資訊
  2. 元資料驗證
    對位元組碼描述的資訊進行了語義分析,包括
    這個類是否有父類(除了object型別,其餘都有)
    這個類的父類是否繼承了不允許被繼承的類(final)
    類的欄位,方法是否與父類產生矛盾(覆蓋final欄位,不合規的過載)
  3. 位元組碼驗證
    主要目的是通過資料流和控制流分析確定程式語義是合法的,複合邏輯的。
    保證任意時刻運算元棧的資料型別與指令程式碼序列都能配合工作,不會出現操作棧放置了一個int型,但是卻按照long型別載入到本地變量表;
    保證跳轉 指令不會跳轉到方法體以外的位元組碼指令上;
    保證方法體中的型別轉換有效(例如把一個子類物件賦值給父類是安全的,但是反過來是危險的)
  4. 符號引用驗證
    最後一階段,虛擬機器將符號引用轉化為直接引用,轉化的動作在解析的階段發生。符號引用校驗可以看做是對類自身以外(常量池中的各種符號引用)的資訊進行匹配性校驗,通常包括一下內容
    符號引用通過字串描述的全限定名能否找到對應的類;
    指定的類中是否存在符合方法的欄位描述以及簡單名稱所描述的方法和欄位;
    符號引用中的類,方法,屬性的訪問性是否能被當前類訪問

驗證階段非重要但是不是一定必要,確保程式碼是可靠的,可以通過-Xverify:none引數關閉驗證

準備

正式為類變數分配記憶體並設定類變數初始值的階段,這些變數所使用的記憶體都會在方法區進行分配(該階段進行記憶體分配的僅僅包括static變數,不包括例項變數)。其次這裡所說的初始化“通常情況”下是資料型別的零值。
非通常情況:如果類欄位的欄位屬性表中存在ConstantValue的屬性,則在準備階段初始化為ConstantValue屬性所指定的值。如final static定義的變數

解析

解析階段是將常量池內的符號引用替換成直接引用的過程

符號引用: 符號引用是以一組符號來描述所引用的目標,可以是任何形式的字面量(CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info),與虛擬機器記憶體佈局無關,引用的目標也不一樣載入到記憶體中。
直接引用:可以是直接指向目標的指標,相對偏移量或者一個間接定位目標的控制代碼,與虛擬機器的記憶體佈局有關。

虛擬機器並未規定解析階段的具體時間,只要求在執行anewarray,checkcast,getfield,getstatic,instanceof,invokedynamic,invokeinterface,invokespecial,invokestatic,invokevirtual,ldc,ldc_w,multianewarray,new,putfield和putstatic這16個用於操作符號引用的位元組碼指令之前,對他們使用的符號引用進行解析。

除了invokedynamic(為了支援動態語言的指令,程式必須執行到這條指定時,解析動作才能進行)指令外,一個符號引用只進行一次解析(在執行時的常量池中記錄直接引用,並把常量標誌為已解析)

解析的型別


介面
欄位
類方法
介面方法
方法型別
方法控制代碼
呼叫點限定符

類或介面的解析

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

  1. 如果C不是陣列型別,虛擬機器將C的全限定名傳遞給D的類載入器去載入這個類C(遞迴),一旦載入過程異常,解析過程失敗
  2. 如果C是陣列型別,並且資料的元素型別為物件,也就是N的描述符為[Ljava/lang/Integer,那將按照第1點的規則載入陣列元素型別。如果N的描述符如前假設,則載入的元素型別為java.lang.Integer,接著虛擬機器生成一個代表此陣列維度和元素的陣列物件
  3. 如果上述步驟正常,C在虛擬機器中已經成為了有限的類或介面了,但在解析之前要進行符號引用驗證,確認D具備C的訪問許可權。

欄位的解析

解析欄位的符號引用之前,先要對欄位表內的Class_index中索引的類或介面的符號引用進行解析
將這個欄位所屬的類或介面用C表示

  1. 如果C本身包含的簡單名稱和欄位描述符與目標匹配,則返回
  2. 否則,如果C實現了介面,則按照繼承關係從下往上遞迴搜尋各個介面
  3. 否則,查詢失敗

類方法解析

第一步也是先解析方法所在的類,類方法和介面方法是分開的

  1. 查詢是否有簡單名稱和描述符與目標複合的
  2. 否則,在父類中遞迴查詢
  3. 介面,抽象類之中查詢,找到丟擲異常

介面方法解析

同類方法的介面,只不過是在介面中匹配

初始化

在準備階段,變數以及賦過系統要求的初始化。而在初始化階段,則根據程式設計師的主觀設計去初始化類變數和其它資源。
初始化即是執行類構造器< clinit>()方法的過程。

< clinit>()方法由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的,編譯收集的順序由語句在原始檔中出現的順序決定。

靜態語句塊只能訪問定義在靜態語句塊之前的變數,定義在之後的可賦值但是不能訪問

虛擬機器在呼叫< clinit>()方法之前保證父類的< clinit>()方法已經執行完畢。虛擬機器保證一個類的< clinit>()方法在多執行緒環境中被正確地加鎖,同步。

類與類載入器

對於任意一個類,都需要由載入它的類載入器和這個類本身一同確立其在Java虛擬機器中的唯一性。

雙親委派模型

Bootstrap ClassLoader啟動器載入器,載入\lib目錄下的jar;
Extension ClassLoader擴充套件類載入器,載入\lib\ext目錄下的jar;
Application ClassLoader應用程式類載入器,載入使用者路徑下的類庫