1. 程式人生 > >【Java虛擬機器】類載入

【Java虛擬機器】類載入

類載入

類載入的時機

類載入宣告週期

類從被載入到虛擬機器記憶體開始,到卸載出記憶體為止,整個生命週期包括:載入(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和解除安裝(Unloading)7個階段。

其中驗證、準備、解析三個部分稱為連線(Liking)。

這個7個階段的發生順序如下圖所示:
在這裡插入圖片描述
載入、驗證、準備、初始化和解除安裝這5個階段的順序是確定的,是按部就班進行的,而解析階段不一定:它在某些情況下可以在初始化之後再開始,這是為了支援Java語言的執行時繫結。

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

類初始何時進行

  1. 遇到new、getstatic、putstatic或invokestatic這4條指令時,也就是使用new關鍵字例項化物件的時候、讀取或設定一個類的靜態欄位(被 static final 修飾的、已在編譯期把結果放入常量池的靜態欄位除外)的時候,以及呼叫一個類的靜態方法的時候
  2. 使用java.lang.reflect包的方法對類進行反射呼叫的時候
  3. 初始化一個類的時候,如果發現其父類還沒有進行初始化的時候
  4. 當虛擬機器啟動時,使用者需要制定一個要執行的主類(包含main() 方法的那個類)
  5. 如果一個java.lang.invoke.MethodHandle例項最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制代碼的時候

注意:
一個介面初始化時,並不要求父介面全部都完成了初始化,只有在真正使用到了父介面的時候才會初始化

類載入的過程

類載入的全過程,也就是記載、驗證、準備、解析和初始化這5個階段所執行的具體動作。

載入

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

驗證

檔案格式驗證

是否符合class檔案格式的規範

  • 是否以魔數0xcafebabe開頭
  • 主、次版本號是否在當前虛擬機器處理範圍之內
  • 常量池的常量中是否有不被支援的常量型別(檢查常量tag標誌)
  • 指向常量的各種索引值中是否有指向不存在的常量或不符合型別的常量
  • CONSTANT_Utf8_info型的常量中是否有不符合UTF8編碼的資料
  • class檔案中各個部分及檔案本身是否有被刪除的或附加的其他資訊
    … …

元資料驗證

是否符合Java語言規範

  • 這個類是否有父類(除了object類,其他的類都應該有父類)
  • 這個類的父類是否繼承了不允許被繼承的類(被final修飾的類)
  • 如果這個類不是抽象類,是否實現了其父類或介面之中要求實現的所有方法
  • 類中的欄位、方法是否與父類產生矛盾(例如覆蓋了父類final欄位,或者出現不符合規則的方法過載)
    … …

位元組碼驗證

程式語義是否合法、符合邏輯

  • 保證任意時刻運算元棧的資料型別與指令程式碼序列都能配合工作,例如不會出現這樣的情況:在運算元棧放置了一個int資料型別,使用時卻按long型別來載入如本地變量表中
  • 保證跳轉指令不會跳轉到方法體以外的位元組碼指令上
  • 保證方法體中的型別轉換是有效的
    … …

符號引用驗證

在解析階段進行驗證

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

準備

為類變數分配記憶體並設定類變數初始值的階段,在方法區進行分配。

這裡所說的初始值“通常情況下”是資料型別的零值。特殊情況就是如果欄位屬性表中存在ConstantValue屬性,那麼準備階段變數就會初始化為ConstantValue屬性所指定的值。

解析

解析階段就是將常量池內的符號引用替換為直接引用的過程。直接引用可以是指向目標的指標、相對偏移量或是一個能間接定位到目標的控制代碼。如果有了直接引用,那麼引用的目標必定已經在記憶體中存在。

類或介面解析

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

  1. 如果C不是一個數組型別,那虛擬機器將會把代表N的全限定名傳遞給D的類載入器去載入這個類C。在載入過程中,由於元資料驗證、位元組碼驗證的需要,有可能觸發其他相關類的載入動作。一旦這個載入過程出現了任何異常,解析過程宣告失敗
  2. 如果C是一個數組型別,並且陣列的型別的元素型別是物件,則將會按照第1點的規則載入陣列元素型別,接著
  3. 如果上面的步驟沒有出現任何異常,那麼C在虛擬機器中實際上已經成為一個有效的類或介面了

欄位解析

首先對欄位所屬的類或介面進行解析,如果解析成功,用C表示所屬的類或介面,按照以下步驟對C進行後續欄位的搜尋

  1. 如果C本身就包含了簡單名稱和欄位描述符都與目標相匹配的欄位,則返回這個欄位的直接引用
  2. 否則,如果在C中實現了介面,則按照繼承關係從下向上遞迴搜尋各個介面和它的父介面,如果介面包含了簡單名稱和欄位描述符都與目標相匹配的欄位,則返回這個欄位的直接引用
  3. 否則,如果C不是java.lang.Object的話,將會按照繼承關係從下往上遞迴搜尋其父類,如果父類中包含了簡單名稱和欄位描述符都與目標匹配的欄位,則返回這個欄位的直接引用
  4. 否則,查詢失敗

類方法解析

首先對方法所屬的類進行解析,如果解析成功,用C表示所屬的類,按照以下步驟對C進行後續類方法的搜尋

  1. 類方法和介面方法符號引用的常量型別定義是分開的,如果在類方法表中發現索引的C是個介面,失敗
  2. 如果通過第1步,在類C中查詢是否有簡單和描述符都與目標相匹配的方法,如果有則放回這個方法的直接引用
  3. 否則,在類C的父類中遞迴查詢是否有簡單和描述符都與目標相匹配的方法,如果有則放回這個方法的直接引用
  4. 否則,在類C實現的介面列表以及它們的父類介面之中遞迴查詢是否有簡單名稱和描述符與目標相匹配的方法,如果存在匹配的方法,說明類C是一個抽象類,失敗
  5. 否則,失敗

介面方法解析

首先對方法所屬的介面進行解析,如果解析成功,用C表示所屬的介面,按照以下步驟對C進行後續介面方法的搜尋

  1. 類方法和介面方法符號引用的常量型別定義是分開的,如果在類方法表中發現索引的C是個類而不是介面,失敗
  2. 否則,在介面C中查詢是否有簡單和描述符都與目標相匹配的方法,如果有則放回這個方法的直接引用
  3. 否則,在介面C的父介面中遞迴查詢是否有簡單和描述符都與目標相匹配的方法,如果有則放回這個方法的直接引用
  4. 否則,失敗

初始化

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

  • <licnit>()方法有編譯器自動收集類中的所有類變數的複製動作和靜態語句塊中的語句合併產生的,收集循序是按照原始檔中出現的順序決定的
  • <licnit>()方法與類的建構函式( <init>())不同,他不需要顯式地呼叫父類構造器,虛擬機器會保證子類的<licnit>()方法執行之前,父類的<licnit>()方法已經執行完畢
  • 介面的<licnit>()方法不需要先執行父介面的<licnit>()方法
  • 虛擬機器會保證<licnit>()方法在多執行緒環境中被正確地加鎖、同步,保證<licnit>()方法只會被執行一次

類載入器

類與類載入器

比較兩個類是否相等,只有在這兩個類是由同一個虛擬機器載入的前提下才有意義,否則,即使這兩個類來源於同一個class檔案,被同一個虛擬機器載入,只要載入它們的類載入不同,那麼這兩個類就必定不相等。

雙親委派模型

在這裡插入圖片描述

  • 啟動類載入器負責lib目錄中的
  • 擴充套件類載入器負責lib\ext目錄中的
  • 應用程式類載入器是程式中預設的類載入器

圖中展示的類載入之間的這種層次關係,稱為類載入器的雙親委派模型。

工作過程:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委託給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求最終都應該產送到頂層的啟動類載入器中,只有當父類載入器反饋自己無法完成這個載入請求時,子載入器才會嘗試自己去載入

參考

  1. 深入理解Java虛擬機器[書籍]