­

JVM類載入機制

JVM類載入機制

1. 類載入的時機

一個類從載入到虛擬機記憶體中開始,到卸載出記憶體位置,將經歷七個階段。

image-20210916102832896

《Java虛擬機規範》嚴格規定了有且只有六種必須立即對類進行初始化的場景。

  1. 遇到new、getstatic、putstatic或invokestatic這四條位元組碼指令時。
    • 使用new實例化對象時
    • 讀取或設置靜態欄位時
    • 調用靜態方法時
  2. 使用java.lang.reflect包的方法對類型進行反射調用的時候。
  3. 當初始化類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
  4. 當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先 初始化這個主類。
  5. 當使用JDK 7新加入的動態語言支援時,如果一個java.lang.invoke.MethodHandle實例最後的解 析結果為REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四種類型的方法句 柄,並且這個方法句柄對應的類沒有進行過初始化,則需要先觸發其初始化。
  6. 當一個介面中定義了JDK 8新加入的默認方法(被default關鍵字修飾的介面方法)時,如果有 這個介面的實現類發生了初始化,那該介面要在其之前被初始化。

2. 類載入的過程

2.1 載入

在載入階段,Java虛擬機需要完成三件事情。

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

這個載入階段是開發人員在類載入過程中可控性最強的階段,它既可以由Java虛擬機中內置的引導類載入器來完成,也可以由用戶自定義的類載入器完成。

2.2 連接

2.2.1 驗證

驗證是連接階段的第一步,目的是檢查位元組流中的資訊符合《Java虛擬機規範》的約束要求。大致上有四個階段。

  • 文件格式驗證。第一階段要驗證位元組流是否符合Class文件格式的規範,並且能被當前版本的虛擬機處理。
  • 元數據驗證。第二階段是對位元組碼描述的資訊進行語義分析。
  • 位元組碼驗證。第三階段主要目的是通過數據流分析和控制流分析,確定程式語義是合法的、符合邏輯的。
  • 符號引用驗證。符號引用驗證可以看作是對類自身以外(常量池中的各種符號引用)的各類資訊進行匹配性校驗,通俗來說就是,該類是否缺少或者被禁止訪問它依賴的某些外部類、方法、欄位等資源。
2.2.2 準備

準備階段是正式為類中定義的變數(即靜態變數,被static修飾的變數)分配記憶體並設置類變數初始值的階段。

從概念上講,這些變數所使用的記憶體都應當在方法區中進行分配,但必須注意到方法區本身是一個邏輯上的區域。

  • 在JDK 7及之前,HotSpot使用永久代來實現方法區時,實現是完全符合這種邏輯概念的;
  • 而在JDK 8及之後,類變數則會隨著Class對象一起存放在Java堆中

image-20210916104411730

2.2.3 解析

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

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

常見的有類或介面的解析、欄位解析、方法解析、介面方法解析。基本遵循一個按繼承關係查找的原則。

2.3 初始化

在初始化階段,會根據程式設計師在Java程式碼中制定的主管計划去初始化類變數和其他資源。初始化階段就是執行類構造器<clinit>()方法的過程。

<clinit>是Javac編譯器的自動生成物,它由編譯器自動收集類中的所有變數的賦值操作和靜態語句塊(static{}塊)中的語句合併產生的。

3. 類載入器

3.1 概念

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

Java中任意一個類都必須有一個獨立的類載入,類和類載入器是一一對應的。我們說兩個類相等,那麼載入他們的類載入器一定相等。

3.2 雙親委派機制

Java虛擬機的角度看只有兩種類載入器

  • 啟動類載入器,是虛擬機的一部分,由C++實現。
  • 其他所有類載入器,由Java實現,繼承自抽象類java.lang.ClassLoader
3.2.1 三層類載入器

image-20210916110416894

  • 啟動類載入器(引導類載入器)。這個類載入器負責將存放在 \lib目錄,或者被-Xbootclasspath參數所指定的路徑中存放的,而且是Java虛擬機能夠 識別的(按照文件名識別,如rt.jar、tools.jar,名字不符合的類庫即使放在lib目錄中也不會被載入)類庫載入到虛擬機的記憶體中。啟動類載入器無法被Java程式直接引用,如果需要把載入請求委派給引導類載入器處理,那麼直接用null代替即可。
  • 擴展類載入器。這個類載入器是在類sun.misc.Launcher$ExtClassLoader 中以Java程式碼的形式實現的。它負責載入\lib\ext目錄中,或者被java.ext.dirs系統變數所指定的路徑中所有的類庫。
  • 應用程式類載入器。這個類載入器由 sun.misc.Launcher$AppClassLoader來實現。由於應用程式類載入器是ClassLoader類中的getSystem$ClassLoader()方法的返回值,所以有些場合中也稱它為「系統類載入器」。它負責載入用戶類路徑 (ClassPath)上所有的類庫,開發者同樣可以直接在程式碼中使用這個類載入器。如果應用程式中沒有 自定義過自己的類載入器,一般情況下這個就是程式中默認的類載入器。
3.1.2 雙親委派機制

image-20210921154505848

上圖中類載入器的層次關係成為類載入器的雙親委派模型。雙親委派模型要求除了頂層的啟動類載入器外,其餘的類載入器都應有自己的父類載入器。不過這裡類載入器之間的父子關係一般不是以繼承(Inheritance)的關係來實現的,而是通常使用組合(Composition)關係來複用父載入器的程式碼。

雙親委派模型的工作過程是:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳送到最頂層的啟動類載入器中,只有當父載入器回饋自己無法完成這個載入請求(它的搜索範圍中沒有找到所需的類)時,子載入器才會嘗試自己去完成載入。

3.3 破壞雙親委派模型

詳見《深入理解Java虛擬機》p285

Java歷史上出現過三次大規模的雙親委派機制被破壞。

  • 雙親委派機制在JDK1.2被引入,而類載入器在Java的第一個版本就已經存在。為了兼容過去的程式碼,JDK1.2後的ClassLoader類中添加了一個新的protected方法findClass()。如果父類載入失敗,會調用自己的findClass()方法完成載入。
  • 第二個問題出現於雙親委派模型天然的缺陷。假如那些非常基礎的類需要調用用戶程式碼應該怎麼辦呢。那麼不就是由父類載入器去請求了子類載入器嗎。一個典型的例子就是JNDI服務。Java引入了執行緒上下文類載入器的機制,從父類中繼承一個類載入器,載入所需的服務程式碼。
  • 第三個問題是由於用戶對程式動態性的追求。出現在OSGi技術中,它可以實現模組化熱部署。當收到類載入求時,OSGi按照下面的規則進行類搜索,而不是雙親委派模型。

image-20210916114223958

Tags: