每天學習一點 JVM 之:類載入機制

LemonYang發表於2016-12-05

關於JVM系列的文章,都是在讀了《深入理解java虛擬機器》一書之後的讀書筆記總結。

在JAVA語言中,型別的載入、連線和初始化過程都是在程式執行期間完成的,JAVA動態擴充的語言特性就是依賴於執行期動態載入和動態連線這個特點實現的。類從被載入到虛擬機器記憶體中開始到解除安裝出記憶體為止,整個生命週期如下圖所示:

每天學習一點 JVM 之:類載入機制

類載入時機

java虛擬機器規範嚴格規定了有且只有下面五種情況必須立即對類進行“初始化”(載入、驗證、準備這三個步驟需要在此之前開始):

  • 遇到new、getstatic、putstatic或invokestatic這四條子節碼指令的時候,如果類沒有進行過初始化,則需要先觸發其初始化。最常見的場景是:使用new關鍵字例項化物件的時候、讀取或者設定一個類的靜態欄位的時候(被final修飾,已在編譯期把結果放入常量池的靜態欄位除外)以及呼叫一個類的靜態方法的時候。

  • 使用java.lang.reflect包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化,則需要先觸發其初始化。

  • 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發父類的初始化。(一個介面在初始化的時候,並不要求其父藉口全部都完成了初始化,只有真正使用父介面的時候才會初始化)

  • 當初始化一個類的時候,使用者需要指定一個主要執行的主類,虛擬機器會先初始化這個主類。

  • 當使用jdk1.7的動態語言支援時,如果一個java.lang.invoke.methodhandle例項最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制程式碼,並且這個方法控制程式碼對應的類沒有進行過初始化,則需要先觸發其初始化。

類載入過程

載入

在載入階段,虛擬機器需要完成以下三件事情:

  • 通過一個類的許可權定名來獲取定義此類的二進位制位元組流。

  • 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。

  • 在記憶體中生成一個代表這個類的java.lang.class物件,作為方法區這個類的各種資料的訪問入口。

與非陣列類的載入階段不同,陣列類本身不通過類載入器建立,它是由java虛擬機器直接建立的。如果陣列的元件型別時引用型別,那就遞迴的採用以上的載入過程載入這個元件型別嗎,陣列將在載入該元件型別的類載入器的類名稱上被標識。如果陣列的元件型別不是引用型別,java虛擬機器將會把陣列標記為於引導類載入器相關聯

類載入完成以後,虛擬機器外部的二進位制位元組流就按照虛擬機器所需的格式儲存在方法區之中。

載入階段與連線階段的部分內容時交叉進行的,載入階段尚未完成,連線階段就已經開始,但是這兩個階段的開始時間依然保持著固定的先後順序。

驗證

驗證階段是連線階段的第一步,會完成以下四個階段的檢驗動作。

  1. 檔案格式驗證。如:是否以魔術數0xcafebabe開頭,主次版本號是否在當前虛擬機器處理範圍之內等
  2. 後設資料驗證。對類的後設資料進行語義校驗,保證不存在不符合java語言規範的後設資料資訊
  3. 位元組碼驗證。通過資料流和控制流分析,確定程式語義是合法的、符合邏輯的
  4. 符號引用驗證。發生在虛擬機器將符號引用轉化為直接引用的時候,這個轉化動作將在解析階段中發生。這可以看作是對類自身以外的資訊(常量池中的各種符號引用)進行匹配行檢驗。

準備

準備階段正式為 類變數 分配記憶體並賦初始值的階段,這些變數所使用的記憶體都將在方法區中進行分配。

解析

解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。

  • 符號引用

    符號引用以一組符號來描述所引用的目標,符號可以好似任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機器實現的記憶體佈局無關,引用的目標不一定已經載入到記憶體中。

  • 直接引用

    直接引用可以是直接指向目標的指標、相對偏移量或是一個能間接定位到目標的控制程式碼。直接引用是和虛擬機器實現的記憶體佈局相關的,同一個符號引用在不同的虛擬機器例項上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經在記憶體中存在。

解析動作主要針對類或介面、欄位、類方法、介面方法四類符號引用進行,分別對應於常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info四種常量型別。

  • 欄位解析

    對欄位進行解析時,會先在本類中查詢是否包含有簡單名稱和欄位描述符都與目標相匹配的欄位,如果有,則查詢結束;如果沒有,則會按照繼承關係從下往上遞迴搜尋該類所實現的各個介面和它們的父介面,還沒有,則按照繼承關係從下往上遞迴搜尋其父類,直至查詢結束。

  • 類方法解析

    對類方法的解析與對欄位解析的搜尋步驟差不多,只是多了判斷該方法所處的是類還是介面的步驟,而且對類方法的匹配搜尋,是先搜尋父類,再搜尋介面。

  • 介面方法解析

    與類方法解析步驟類似,只是介面不會有父類,因此,只遞迴向上搜尋父介面就行了。

初始化

初始化階段是執行類構造器< clinit >()方法的過程。

< clinit >()方法是由編譯器自動收集類中的靜態變數的賦值動作和靜態語句塊中的語句合併產生的,編譯器收集的順序是由語句在原始檔中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變數,定義在它之後的變數,在前面的靜態語句塊可以賦值,但是不能訪問。

< clinit >()方法與類的建構函式不同,它不需要顯式地呼叫父類構造器,虛擬機器會保證在子類的< clinit >()方法執行之前,父類的< clinit >()方法方法已經執行完畢。

簡單總結,感謝你寶貴的時間閱讀這篇文章!

本文參考了這篇博文
【深入Java虛擬機器】之四:類載入機制

相關文章