《深入理解 Java 虛擬機》讀書筆記:虛擬機類載入機制

正文

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

一、類載入的時機

1、類的生命周期

載入 -> 連接(驗證、準備、解析) -> 初始化 -> 使用 -> 卸載

載入、驗證、準備、初始化和卸載這 5 個階段的順序是確定的,類的載入過程必須按這種順序按部就班地開始。解析階段則不一定,它在某些情況可以在初始化之後再開始,這是為了支援 Java 語言的運行時綁定(也稱動態綁定或晚期綁定)。

這些階段通常是互相交叉地混合式進行,通常會在一個階段執行的過程中調用、激活另外一個階段。

2、類的初始化時機

  • 遇到 new(實例化對象)、getstatic(讀取類的靜態欄位)、putstatic(設置類的靜態欄位)、invokestatic(調用類的靜態方法) 這 4 條位元組碼指令時。
  • 使用 java.lang.reflect 包的方法對類進行反射調用時。
  • 初始化一個類的子類時。
  • 虛擬機啟動時,會先初始化主類(main 方法所在的類)。
  • 使用 java.lang.invoke.MethodHandle 獲取類的方法句柄時。

二、類載入的過程

1、載入

「載入」是「類載入」過程的一個階段,在載入階段,虛擬機需要完成 3 件事:

  • 通過類的全限定名獲取定義此類的二進位位元組流。
  • 將這個位元組流所代表的靜態存儲結構,轉化為方法區的運行時數據結構。
  • 在記憶體中生成一個代表這個類的 java.lang.Class 對象,作為方法區這個類的各種數據的訪問入口。

2、驗證

這一階段的目的是為了確保 Class 文件的位元組流中包含的資訊符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。驗證階段大致包含 4 個階段的檢驗動作。

(1)文件格式驗證

驗證位元組流是否符合 Class 文件格式的規範,並且能被當前版本的虛擬機處理。

驗證內容:

  • 是否以魔數 0xCAFEBABE 開頭。
  • 主、次版本號是否在當前虛擬機處理範圍之內。
  • 常量池的常量中是否有不被支援的常量類型。

主要目的是保證輸入的位元組流能正確解析並存儲於方法區內,格式上符合描述一個 Java 類型資訊的要求。

該階段的驗證是基於位元組流進行的,只有驗證通過了,位元組流才會進入記憶體的方法區中進行存儲。所以後面 3 個驗證階段都是基於方法區的存儲結構進行的。

(2)元數據驗證

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

驗證內容:

  • 這個類是否有父類(除 java.lang.Object 外,所有類都應當有父類)。
  • 這個類的父類是否繼承了不允許被繼承的類(被 final 修飾的類)。
  • 如果這個類不是抽象類,是否實現了其父類或介面中要求實現的所有方法。

主要目的是對類的元數據資訊進行語義校驗,保證不存在不符合 Java 語言規範的元數據資訊。

(3)位元組碼驗證

對類的方法體進行檢驗分析,保證類的方法在運行時不會做出危害虛擬機安全的事件。

驗證內容:

  • 保證任意時刻操作數棧的數據類型與指令程式碼序列都能配合工作。
  • 保證跳轉指令不會跳轉到方法體以外的位元組碼指令上。
  • 保證方法體中的類型轉換是有效的。

主要目的是通過數據流和控制流分析,確定程式語義是合法的、符合邏輯的。

(4) 符號引用驗證

對類自身以外(常量池中的各種符合引用)的資訊進行匹配性校驗。這一階段發生在虛擬機將符號引用轉化為直接引用時。

驗證內容:

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

主要目的是確保解析動作能正常執行。

3、準備

準備階段是為類變數分配記憶體並設置初始值的階段。

有兩點需要強調一下:

  • 進行記憶體分配的僅包括類變數,而不包括實例變數。
  • 初始值通常是數據類型的零值。

4、解析

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

  • 符號引用:以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。
  • 直接引用:可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。

解析動作主要針對類或介面、欄位、類方法、介面方法、方法類型、方法句柄和調用點限定符 7 類符號引用進行,分別對應常量池的 CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info 和 CONSTANT_InvokeDynamic_info 7 種常量類型。

5、初始化

初始化階段是根據程式設計師的主觀計划去初始化類變數和其他資源的階段。或者從另一個角度表達,初始化階段是執行類構造器 clinit() 方法的過程。

到了初始化階段,才真正開始執行類中定義的 Java 程式程式碼(或者說位元組碼)。

三、類載入器

虛擬機設計團隊把類載入階段中的「通過類的全限定名獲取定義此類的二進位位元組流」這個動作放到 Java 虛擬機外部去實現,以便讓應用程式自己決定如何去獲取所需的類。實現這個動作的程式碼模組稱為「類載入器」。

1、類與類載入器

對於任意一個類,都需要由載入它的類載入器(每一個類載入器都有一個獨立的類名稱空間)和這個類本身,一同確立其在 Java 虛擬機中的唯一性。

換句話說,比較兩個類是否「相等」,只有在這兩個類是由同一個類載入器載入的前提下才有意義。

2、類載入器分類

從 Java 虛擬機角度講,只存在兩種不同的類載入器:

  • 啟動類載入器:使用 C++ 語言實現,是虛擬機自身的一部分。
  • 其他類載入器:由 Java 語言實現,獨立於虛擬機外部。

從 Java 開發人員角度講,可分為三種類載入器:

  • 啟動類載入器(Bootstrap ClassLoader):負責將存放在 <JAVA_HOME>lib 目錄的,或者 -Xbootclasspath 參數所指定路徑中的,能被虛擬機識別的類庫載入到虛擬機記憶體中。
    啟動類載入器無法被 Java 程式直接引用。
  • 擴展類載入器(Extension ClassLoader):由 sun.misc.Launcher$ExtClassLoader 實現,負責載入 <JAVA_HOME>libext 目錄中的,或者 java.ext.dirs 系統變數所指定路徑中的所有類庫。
    開發者可直接使用擴展類載入器。
  • 應用程式類載入器(Application ClassLoader):由 sun.misc.Launcher$AppClassLoader 實現,負責載入用戶類路徑上所指定的類庫。
    開發者可直接使用應用程式類載入器。

3、雙親委派模型

如果一個類載入器收到類載入的請求,它會先把這個請求委派給父載入器去完成,而不會自己去嘗試載入這個類。只有父載入器無法完成這個載入請求時,子載入器才會嘗試自己去載入。