JVM 類加載機制

學習導圖

JVM 類加載機制

一.為什麼要學習類加載機制?

今天想跟大家嘮嗑嘮嗑 Java 的類加載機制,這是 Java 的一個很重要的創新點,曾經也是 Java 流行的重要原因之一。

Oracle 當初引入這個機制是為了滿足 Java Applet 開發的需求, JVM 咬咬牙引入了 Java 類加載機制,後來的基於 Jvm 的動態部署,插件化開發包括大家熱議的熱修復,總之很多後來的技術都源於在 JVM 中引入了類加載器。

如今,類加載機制也在各個領域大放異彩,在面試中,由類加載機制所衍生出來各類面試題也層出不窮。

所以,我們要了解下類加載機制,為工作中或者是面試中實際的需要打好良好的基礎。

二.核心知識點歸納

2.1 概述

Q1:JVM類加載機制定義:

虛擬機把描述類的數據從 Class 文件 加載 到內存,並對數據進行 校驗轉換解析

初始化 ,最終形成可被虛擬機直接使用的 Java 類型的過程

Q2:特性

運行期類加載。即在 Java 語言裡面,類型的加載、連接和初始化過程都是在程序 運行期 完成的,從而通過犧牲一些性能開銷來換取 Java 程序的高度靈活性

什麼是運行期,什麼是編譯期?

  • 編譯期 是指編譯器將 源代碼翻譯機器能識別的代碼 , Java 被編譯為 Jvm 認識的 字節碼文件
  • 運行期 則是指 Java 代碼的 運行 過程

JVM 運行期動態加載+動態連接-> Java 的動態擴展特性

2.2 類加載的過程

類從被加載到虛擬機內存中開始、到卸載出內存為止,整個生命週期包括七個階段:

  • 加載
  • 驗證
  • 準備
  • 解析
  • 初始化
  • 使用
  • 卸載

其中,驗證、準備、解析這3個部分統稱為 連接 ,流程如下圖:

JVM 類加載機制

注意:

<code>『加載』->『驗證』->『準備』->『初始化』->『卸載』這五個階段的順序是確定的,而『解析』可能為了支持Java的動態綁定會在『初始化』後才開始
上述階段通常都是互相交叉地混合式進行的,比如會在一個階段執行的過程中調用、激活另外一個階段/<code>

2.2.1 加載

Q1:任務

  • 通過類的全限定名來獲取定義此類的 二進制字節流 。如從 ZIP 包讀取、從網絡中獲取、通過運行時計算生成、由其他文件生成、從數據庫中讀取等等途徑......
  • 將該二進制字節流所代表的 靜態存儲結構 轉化為 方法區運行時數據結構 ,該數據存儲數據結構由虛擬機實現自行定義
  • 在內存中生成一個代表這個類的 java.lang.Class 對象,它將作為程序訪問方法區中的這些類型數據的外部接口

2.2.2 驗證

  • 連接 階段的 第一步 ,且工作量在 JVM 類加載子系統中佔了相當大的一部分
  • 目的:為了 確保 Class 文件的字節流中包含的信息 符合 當前 虛擬機的要求 ,並且 不會危害虛擬機自身的安全

由此可見,它能直接決定 JVM 能否承受惡意代碼的攻擊,因此驗證階段 很重要 ,但由於它對程序運行期沒有影響,並 不一定必要 ,可以考慮使用 -Xverify:none 參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。

  • 檢驗過程包括下面四個階段:A.文件格式驗證:內容:驗證 字節流是否符合 Class 文件格式的規範
    、以及是否能被 當前版本的虛擬機處理目的:保證輸入的 字節流 能正確地解析並存儲於 方法區 之內,且格式上符合描述一個 Java 類型信息的要求。只有保證二進制字節流通過了該驗證後,它才會進入內存的方法區中進行存儲,所以 後續3個驗證階段全部是基於方法區 而不是字節流了例子:是否以魔數 0xCAFEBABE 開頭主次版本號是否在 JVM 接受範圍內索引值是否有指向不存在/不符合類型的常量......B.元數據驗證:內容:對字節碼描述的信息進行 語義 分析,以保證其描述的信息符合 Java 語言規範的要求目的:對類的 元數據信息 進行語義校驗,保證不存在不符合 Java 語言規範的元數據信息例子:類是否有父類(除了 java.lang.Object 之外,所有類都應有父類)父類是否繼承了不允許被繼承的類( final 修飾的類)如果該類不是抽象類,是否實現了其父類或接口中要求實現的所有方法......​ C.字節碼驗證:是驗證過程中 最複雜 的一個階段內容:對類的 方法體
    進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的事件目的:通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的例子:保證任意時刻操作數棧的數據類型與指令代碼序列都能配合工作,例如不會出現“在操作數棧的數據類型中放置了 int 類型的數據,使用時卻按 long 類型來載入本地變量表中”保證任何跳轉指令都不會跳轉到方法體外的字節碼指令上......​ D.符號引用驗證:內容:對 類自身以外(如常量池中的各種符號引用)的信息 進行匹配性校驗目的:確保解析 動作能正常執行 ,如果無法通過符號引用驗證,那麼將會拋出一個 java.lang.IncompatibleClassChangeError 異常的子類注意:該驗證發生在虛擬機將 符號引用 轉化為 直接引用 的時候,即『 解析 』階段

2.2.3 準備

Q1:任務

  • 為類變量(靜態變量) 分配內存
    因為這裡的變量是由方法區分配內存 的,所以 僅包括類變量 而不包括實例變量,後者將會在對象實例化時隨著對象一起分配在 Java 堆中
  • 設置類變量 初始值 :通常情況下零值

2.2.4 解析

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

  • 符號引用:以一組符號來描述所引用的目標
  • 可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可
  • 與虛擬機實現的內存佈局無關,因為符號引用的字面量形式明確定義在 Java 虛擬機規範的 Class 文件格式中,所以即使各種虛擬機實現的內存佈局不同,但是能接受符號引用都是一致的
  • 直接引用:
  • 可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄
  • 與虛擬機實現的內存佈局相關,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不同
  • 發生時間: JVM 會根據需要來判斷,是在類被加載器 加載時 就對常量池中的符號引用進行解析,還是等到一個符號引用將要被 使用前 才去解析
  • 解析動作:有七類符號及其對應在常量池的七種常量類型
  • 類或接口 ( CONSTANT_Class_info )
  • 字段 ( CONSTANT_Fieldref_info )
  • 類方法 ( CONSTANT_Methodref_info )
  • 接口方法 ( CONSTANT_InterfaceMethodref_info )
  • 方法類型 ( CONSTANT_MethodType_info )
  • 方法句柄 ( CONSTANT_MethodHandle_info )
  • 調用點限定符 ( CONSTANT_InvokeDynamic_info )

舉個例子,設當前代碼所處的為類 D ,把一個從未解析過的 符號引用 N 解析為一個 類或接口 C 的直接引用 ,解析過程分三步:

  • 若 C 不是數組類型: JVM 將會把代表 N 的全限定名傳遞給 D 類加載器去加載這個類 C 。在加載過程中,由於 元數據驗證字節碼驗證 的需要,又可能觸發其他相關類的加載動作。一旦這個加載過程出現了任何異常,解析過程就宣告失敗。
  • 若 C 是數組類型且數組元素類型為對象: JVM 也會按照上述規則加載數組元素類型
  • 若上述步驟無任何異常:此時 C 在 JVM 中已成為一個有效的類或接口,但在解析完成前還需進行 符號引用驗證 ,來確認 D 是否具備對 C 的訪問權限。如果發現不具備訪問權限,將拋出 java.lang.IllegalAccessError 異常

Q1:字段(成員變量/域)和屬性有什麼區別?

  • 屬性,是指對象的屬性,對於 JavaBean 來說,是 getXXX 方法定義的
  • 字段,是成員變量
<code>class Person{
private String mingzi; //mingzi是字段,一般來說字段和屬性是相同的,但是這個例子是特例
public String getName(){ //name是屬性
return mingzi:
}
public void setName(){
mingzi= "張三";
}
}/<code>

2.2.5 初始化

  • 是類加載過程的最後一步,會開始真正執行類中定義的 Java 代碼。而之前的類加載過程中,除了在『 加載 』階段用戶應用程序可通過
    自定義類加載器 參與之外, 其餘階段均由虛擬機主導和控制
  • 與『準備』階段的 區分
<code>準備階段:變量賦初始零值
初始化階段:根據Java程序的設定去初始化類變量和其他資源,或者說是執行類構造器clinit的過程/<code>

clinit :由編譯器自動收集類中的所有 類變量(靜態變量)的賦值動作 和靜態語句塊 static{} 中的語句合併產生

  • 線程安全 的,在多線程環境中被正確地加鎖、同步
  • 對於類或接口來說是 必需的,如果一個類中 沒有靜態語句塊 ,也沒有對變量的賦值操作,那麼編譯器可以不為這個類生成 clinit
  • 接口與類不同的是
    ,執行接口的 clinit 不需要先執行父接口 的 clinit ,只有當父接口中定義的變量使用時,父接口才會初始化。另外, 接口的實現類在初始化時 也一樣不會執行接口的 clinit
  • 在虛擬機規範中,規定了有且只有五種情況 必須立即 對類進行『 初始化 』:
  • 遇到 new 、 getstatic 、 putstatic 或 invokestatic 這4條字節碼指令時
  • 使用 java.lang.reflect 包的方法對類進行反射調用的時候
  • 當初始化一個類的時候,若發現其父類還未進行初始化,需先觸發其父類的初始化
  • 在虛擬機啟動時,需指定一個要執行的 主類 ,虛擬機會先初始化它
  • 當使用 JDK1.7 的動態語言支持時,若一個 java.lang.invoke.MethodHandle 實例最後的解析結果為 REF_getStatic 、 REF_putStatic 、 REF_invokeStatic 的方法句柄,且這個方法句柄所對應的類未進行初始化,需先觸發其初始化。

2.3 類加載器&雙親委派模型

每個類加載器,都擁有一個獨立的命名空間,它不僅用於加載類,還和這個類本身一起作為在 JVM 中的唯一標識。所以比較兩個類是否相等,只要看它們是否由同一個 類加載器 加載,即使它們來源於同一個 Class 文件且被同一個 JVM 加載,只要加載它們的 類加載器不同,這兩個類就必定不相等

2.3.1 類加載器

從 JVM 的角度,可將類加載器分為兩種:

  • 啟動類加載器
  • 由 C++ 語言實現,是虛擬機自身的一部分
  • 負責加載存放在 <JAVA_HOME>\\lib 目錄中、或被 -Xbootclasspath 參數所指定路徑中的、且可被虛擬機識別的類庫
  • 無法被 Java 程序直接引用,如果自定義類加載器想要把加載請求委派給引導類加載器的話,可直接用 null 代替
  • 其他類加載器:由 Java 語言實現,獨立於虛擬機外部,並且全都繼承自抽象類 java.lang.ClassLoader ,可被 Java 程序直接引用。常見幾種:
  • 擴展類加載器A.由 sun.misc.Launcher$ExtClassLoader 實現B.負責加載 <JAVA_HOME>\\lib\\ext 目錄中的、或者被 java.ext.dirs 系統變量所指定的路徑中的所有類庫
  • 應用程序類加載器A.是 默認 的類加載器,是 ClassLoader#getSystemClassLoader() 的返回值,故又稱為 系統類加載器B.由 sun.misc.Launcher$App-ClassLoader 實現C.負責加載用戶類路徑上所指定的類庫
  • 自定義類加載器:如果以上類加載起不能滿足需求,可自定義
JVM 類加載機制

需要注意的是:雖然 數組類 不通過類加載器創建而是由 JVM 直接創建的,但仍與類加載器有密切關係,因為 數組類的元素類型最終還要靠類加載器去創建

2.3.2 雙親委派模型

  • 定義:表示類加載器之間的層次關係
  • 前提 :除了頂層啟動類加載器外, 其餘類加載器都應當有自己的父類加載器 ,且它們之間關係一般不會以 繼承 關係來實現,而是通過 組合 關係來複用父加載器的代碼
  • 工作過程 :若一個類加載器收到了類加載的請求,它先會把這個請求 委派 給父類加載器,並向上傳遞,最終請求都傳送到頂層的啟動類加載器中。只有當父加載器反饋自己無法完成這個加載請求時,子加載器才會嘗試自己去加載
  • 注意 :不是一個強制性的約束模型,而是 Java 設計者推薦給開發者的一種類加載器實現方式
  • 優點 :類會隨著它的類加載器一起具備帶有 優先級 的層次關係,可保證 Java 程序的穩定運作;實現簡單,所有實現代碼都集中在 java.lang.ClassLoader的loadClass() 中

比如,某些類加載器要加載 java.lang.Object 類,最終都會委派給最頂端的啟動類加載器去加載,這樣 Object 類在程序的各種類加載器環境中都是同一個類。

相反,系統中將會出現多個不同的 Object 類, Java 類型體系中最基礎的行為也就無法保證,應用程序也將會變得一片混亂


分享到:


相關文章: