1. 程式人生 > >讀薄《深入理解 Java 虛擬機器》虛擬機器類載入機制

讀薄《深入理解 Java 虛擬機器》虛擬機器類載入機制

#虛擬機器類載入機制

類被載入的生命週期包括

載入→驗證→準備→解析→初始化→使用→解除安裝

解析階段在某些情況下可以在初始化階段之後開始,這是為了支援 Java 語言的執行時繫結。

虛擬機器規範嚴格規定了有且只有 5 種情況必須立即對類進行初始化。

  • 遇到 new,getstatic,putstatic 或 invokestatic 這 4 條位元組碼指令時,如果類沒有進行過初始化,則需要先出發初始化。
  • 使用反射呼叫的時候,如果類沒有進行過初始化,則需要先觸發初始化。
  • 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則先出發父類初始化。
  • 當虛擬機器啟動的時候,使用者需要制定一個主類(包含 main() 方法的類),虛擬機器會自動初始化這個類
  • 使用 JDK 1.7 動態語言支援的時候,如果一個 java.lang.invoke.MethodHandle 例項最後解析結果是 REF_getStatic,REF_putStatic,REF_invokeStatic 的方法控制代碼,並且這個方法的控制代碼所對應的類沒有進行過初始化

除了這 5 種情況以外,所有的類引用都為被動引用。

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

載入

載入過程

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

注意在第一條虛擬機器並沒有規定二進位制位元組流一定要從 Class 檔案讀取,虛擬機器可以從任意地方讀取。許多的技術都是建立在這一個基礎上的。

比如動態代理技術,二進位制位元組流是執行時計算生成的。

載入階段既可以使用系統提供的引導類載入器完成也可以由使用者自定義的載入器去完成。

對於陣列型別,陣列類本身不通過類載入器建立,它是由 Java 虛擬機器直接建立的。因為陣列類的元素型別最終是要靠類載入器去建立,一個數組類建立過程遵循以下規則:

驗證

Java 程式碼編譯的 Class 對於虛擬機器是無害的,但是 Class 檔案位元組流來源多樣,無法確定是否會造成虛擬機器崩潰。

如果所執行的全部程式碼都已經被反覆使用和驗證過,可以在實施階段用 -Xverify:none 引數來關閉大部分的類驗證措施,縮短載入時間。

驗證過程分為

檔案格式驗證

這個階段驗證位元組流是否符合 Class 檔案格式的規範,包括

  • 是否以魔數 0xCAFEBABE 開頭
  • 主次版本號是否在當前虛擬機器處理範圍之內
  • 常量池的常量是否有不被支援的型別
  • 是否有不符合 UTF-8 編碼的資料

這一階段保證輸入的位元組流能正確地解析並且儲存於方法之內。

元資料驗證

對位元組碼描述的資訊進行語義分析,保證描述的資訊符合 Java 語言規範。

位元組碼驗證

通過位元組流分析,確定語義是合法的,符合邏輯的。保證被校驗類的方法在執行的時候不會做出危害虛擬機器的事情。

  • 保證任意時刻運算元棧的資料型別與指令程式碼序列能配合工作
  • 保證型別轉換是有效的

資料流驗證複雜性非常高,在 JDK 1.6 之後的 Javac 編譯器和 Java 虛擬機器中進行了一項優化,給方法體的 Code 屬性的屬性表中增加了一項名為 StackMapTable 的屬性。

符號引用驗證

最後一個階段驗證將一個符號引用轉為直接引用的時候,對類自身以外的資訊進行匹配校驗

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

準備

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

這個時候只進行對被 static 修飾的變數進行分配記憶體,被分配的變數這時候的值是初始值,除了常量會在這個階段被直接初始化為 ConstantValue 所指定的值。

static int i = 123; // 這個階段初始化為 0 
static final int j = 123; // 這個階段初始化為 123

解析

符號引用:描述所引用的目標,符號可以是任何形式的字面量。

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

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

除了 invokedynamic 以外,虛擬機器可以將翻譯的結果快取下來,避免重複解析。

因為 invokedynamic 的目的是支援動態語言,這裡的動態是指必須等到程式執行到這條指令後解析動作才進行。

初始化

初始化階段是類載入的最後一步,在準備階段,變數被賦為初始值,而在初始化階段,則根據程式設計師給的值去初始化類變數和其他資源。初始化會執行 <clinit>方法

**:**這個方法是由編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊中的語句合併而成的,靜態語句塊只能訪問到定義在靜態語句前的變數,定義在它之後的變數只能賦值不能訪問。

關於 clinit 的一切

  • 子類的 clinit 一定會發生在父類的 clinit 之後

  • cinit 方法對類或介面來說並不是必須的

  • 介面中不能使用靜態語句,但仍然可以有靜態變數,使用靜態變數時不會呼叫父類的 clinit 方法

  • 虛擬機器會保證一個類的 clinit 方法在多執行緒環境中被正確的加鎖同步,如果多個執行緒同時去初始化一個類,那麼只有一個執行緒去執行這個類的 clinit 方法,其他的執行緒會被阻塞住。

類載入器

通過一個類的全限定名來獲取描述此類的二進位制位元組流,這個動作是通過 Java 虛擬機器外部實現的。

對於任意一個類,都需要載入它的類載入器和這個類本身一同確立在其 Java 虛擬機器的唯一性,比較兩個類是否相等,只有被同一個載入器載入的前提下才有意義。

即便是同樣的位元組程式碼,被不同的類載入器載入之後所得到的類,也是不同的。

類載入器種類

啟動類載入器,Bootstrap ClassLoader,載入JACA_HOME\lib,或者被-Xbootclasspath引數限定的類
擴充套件類載入器,Extension ClassLoader,載入\lib\ext,或者被java.ext.dirs系統變數指定的類
應用程式類載入器,Application ClassLoader,載入ClassPath中的類庫
自定義類載入器,通過繼承ClassLoader實現,一般是載入我們的自定義類

雙親委派模型

對於虛擬機器,之存在兩種類載入器,一種是啟動類載入器,這個載入器由 C++ 實現,是虛擬機器的一部分,另外一種是其他所有載入器。獨立於虛擬機器外部,並且繼承自 java.lang.ClassLoader。

雙親委派模型:一個類載入器收到了載入類的請求,它不會去載入這個類而是將這個請求託付給父類載入器。每一個載入器都是如此,所以所有的請求都會傳送到啟動類載入器。當父類載入器載入失敗的時候,這個類載入器才去載入這個類。

雙親委派好處

  • 避免同一個類被多次載入;
  • 每個載入器只能載入自己範圍內的類;

##破壞雙親委派模型

雙親委派模型在 JDK 1.2 的時候才釋出,為了相容以前的 Java 程式碼,ClassLoader 添加了一個新的 findClass() 方法,在這之前,使用者都是重寫 loadClass() 方法來編寫自己的類載入器的。現在不推薦重寫 loadClass 方法,而是應該重寫 findClass() 方法。

上下文類載入器,這個類載入器可以通過 setContextClassLoader() 進行設定,如果建立執行緒時還未設定,它將會從父執行緒中繼承。如果都未設定的話就是預設的載入器了。

有了這個載入器,父類載入器就可以請求子類載入器去完成類載入器的動作。

程式碼熱替換,模組熱部署。 OSGi 實現模組化熱部署的關鍵是它自定義的類載入器機制的實現,每一個程式模組都有一個自己的類載入器,當需要更換一個 Bundle 時,就把 Bundle 連同類載入器一起換掉實現程式碼的熱替換。

在 OSGi 將按照下面的順序進行類搜尋:

  1. 以 java* 開頭的類直接委派給父類載入器載入
  2. 否則將委派列表名單內的類委派給父類載入器
  3. 否則將 Import 列表中的類委派給 Export 這個類的 Bundle 的類載入器載入
  4. 否則查詢 Bundle 的 ClassPath,使用自己的類載入器
  5. 否則查詢類是否在 Fargment Bundle 中,如果在則委派給 Fragment Bundle 的類載入器載入
  6. 否則查詢 Dynamic Import 列表中的 Bundle
  7. 否則查詢失敗

上面的查詢順序只有前兩點符合雙親委派模型,其餘查詢都是在同一等級的類載入器中進行的。