1. 程式人生 > >JVM的類載入機制全面解析

JVM的類載入機制全面解析

什麼是類載入機制

JVM把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被JVM直接使用的Java型別,這就是JVM的類載入機制。
如果你對Class檔案的結構還不熟悉,可以參考之前的文章Class檔案結構全面解析(上)和Class檔案結構全面解析(下)。

歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。

類的生命週期

類從被載入到記憶體中,到被卸載出記憶體,一共分為以下幾步:

  1. 載入(Loading)
  2. 驗證(Verification)
  3. 準備(Preparation)
  4. 解析(Resolution)
  5. 初始化(Initialization)
  6. 使用(Using)
  7. 解除安裝(Unloading)

類載入的全過程,包括其中的載入、驗證、準備、解析、初始化幾個階段。

歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。

載入

載入是類載入的第一階段,在這一步中JVM規範要求完成了以下三件事:

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

以上要求其實並不具體,JVM的具體實現和應用都是比較靈活的。比如:獲取這個類的二進位制位元組流,並沒有說從哪獲取,怎麼獲取,於是就有了從壓縮包中讀取(jar、war、ear)、從網路中獲取(Applet)、執行時計算生成(動態代理)。對於不是陣列的類的載入,我們可以定義自己的類載入器去控制位元組流的獲取方式。但是,對於陣列類就不一樣了,因為陣列類本身不是通過類載入器建立的,而是JVM直接建立的。

歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。

驗證

這一階段是為了保證Class檔案的位元組流中包含的資訊符合當前JVM的要求,並且不危害JVM自身的安全。大致分為以下四個階段:

檔案格式驗證

驗證位元組流是否符合Class檔案格式的規範,能不能被當前JVM處理。驗證點比較多,比如:是否以魔數0xCAFEBABE開頭、主次版本號是否在當前JVM的處理範圍內、常量池的常量是否有不被支援的常量型別、CONSTANT_Utf8_info型別的常量中是否有不符合UTF8編碼的資料等等。這個階段是基於二進位制位元組流進行驗證的,只有這個階段驗證通過了,位元組流才能進入記憶體的方法區儲存。

歡迎關注微信公眾號:萬貓學社

,每週一分享Java技術乾貨。

元資料驗證

這個階段主要是對類的元資料資訊進行語義分析和校驗,保證不存在不符合Java語言規範的元資料資訊。比如:除了java.lang.Object以外的類是否有父類、是否繼承了一個不允許被繼承的類、非抽象類是否實現了其父類或介面中要求實現的所有方法、是否覆蓋了父類的final欄位等等。

歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。

位元組碼校驗

這個階段通過資料流和控制流分析,確保程式語義是合法的、符合邏輯的。比如:放置和使用操作棧時資料型別保證一致、保證跳轉指令不會跳轉到方法體以外的位元組碼指令上、保證方法體中的型別轉換是有效的等等。

歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。

符號引用校驗

這個階段是對類自身以外(常量池中的各種符號引用)的資訊進行匹配性校驗,它發生在解析步驟中,確保解析能正常執行,比如:符號引用中通過字串描述的全限定名是否能找到對應的類、符號引用中的類欄位方法的訪問性是否可以訪問當前類等等。

歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。

準備

在這個階段裡,為靜態變數分配記憶體並設定靜態變數初始值。這裡說的初始值通常情況下,不是程式碼中寫的初始值,而是資料型別的零值。程式碼中寫的初始值,是在初始化階段賦值的。如果是靜態常量(被final修飾),這個階段就會被直接賦值為程式碼中寫的初始值。

歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。

解析

在這個階段裡,JVM把常量池內的符號引用替換為直接引用。符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可,它和JVM實現的記憶體佈局無關。直接引用可以是直接指向目標的指標、相對偏移量或是一個能間接定位到目標的控制代碼,它是和JVM實現的記憶體佈局相關的。如果有了直接引用,那麼引用的目標肯定在記憶體中存在。

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

歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。

初始化

初始化階段才真正開始執行類中定義的位元組碼,也是執行類構造器()方法的過程。()方法是由編譯器自動收集類中的所有靜態變數的賦值動作和靜態語句塊中的語句合併產生的,編譯器收集的順序是用語句在原始檔中出現的順序所決定的,靜態語句塊只能訪問到定義在靜態語句塊之前的變數,定義在它之後的變數,靜態語句塊可以賦值,但是不能訪問。

JVM會保證在子類的()方法執行之前,父類的()方法已經執行完畢,也就是說父類中定義的靜態語句塊要優先於子類的變數賦值操作。如果類沒有靜態語句塊,也沒有對靜態變數賦值,編譯器就不會為這個類生成()方法。介面的()方法不需要先執行父介面的()方法,只有當父介面中定義的變數使用時,父接口才會被初始化。

JVM會保證一個類的()方法在多執行緒環境中被正確地加鎖、同步。如果一個執行緒在執行這個類的()方法,其他執行緒都需要阻塞等待,當()方法執行完後,其他執行緒也不會再次進入()方法。同一個類載入器下,一個類只會被初始化一次。

歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨。

結語

這次我們瞭解了類載入過程的幾個階段,分別是載入、驗證、準備、解析和初始化。載入是把二進位制位元組碼載入記憶體,驗證是校驗位元組流中包含的資訊是否符合當要求,準備是為靜態變數分配記憶體並設定靜態變數初始值,解析是把常量池內的符號引用替換為直接引用,初始化是執行所有靜態變數的賦值動作和靜態語句塊中的語句。

歡迎關注微信公眾號:萬貓學社,每週一分享Java技術乾貨