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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM