Java虛擬機JVM之類加載機制與類加載器

一、類的生命週期

類的生命週期
加載 --> 驗證 --> 準備 --> 解析 --> 初始化 --> 使用 --> 卸載
       |<------- 連接 ------->|
|<------------- 類加載 ---------------->|

類的生命週期一共有 7 個階段,其中前五個階段較爲重要,統稱爲類加載,第 2 ~ 4 階段統稱爲連接,加載和連接中的三個過程開始的順序是固定的,但是執行過程中是可以交叉執行的。

二、類加載的時機

JVM會在第一次主動引用類的時候,加載該類,被動引用時並不會引發類加載的操作。也就是說,JVM並不是在一開始就把一個程序中所有的類都加載到內存中,而是在第一次用到的時候才進行加載。

那麼什麼是主動引用,什麼是被動引用呢?

  • 主動引用
    • 遇到 new、getstatic、putstatic、invokestatic 字節碼指令,例如:
      • 使用 new 實例化對象;
      • 讀取或設置一個類的 static 字段(被 final 修飾的除外);
      • 調用類的靜態方法。
    • 對類進行反射調用;
    • 初始化一個類時,其父類還沒初始化(需先初始化父類);
      • 這點類與接口具有不同的表現,接口初始化時,不要求其父接口完成初始化,只有真正使用父接口時才初始化,如引用父接口中定義的常量。
    • 虛擬機啓動,先初始化包含 main() 函數的主類;
    • JDK 1.7 動態語言支持:一個 java.lang.invoke.MethodHandle 的解析結果爲 REF_getStatic、REF_putStatic、REF_invokeStatic。
  • 被動引用
    • 通過子類引用父類靜態字段,不會導致子類初始化;
    • Array[] arr = new Array[10]; 不會觸發 Array 類初始化;
    • static final VAR 在編譯階段會存入調用類的常量池,通過 ClassName.VAR 引用不會觸發 ClassName 初始化。

也就是說,只有發生主動引用所列出的 5 種情況,一個類纔會被加載到內存中,也就是說類的加載是 lazy-load 的,不到必要時刻是不會提前加載的,畢竟如果將程序運行中永遠用不到的類加載進內存,會佔用方法區中的內存,浪費系統資源。

 

三、類的顯式加載和隱式加載

1、顯示加載

  • 調用 ClassLoader#loadClass(className) 或 Class.forName(className)
  • 兩種顯示加載 .class 文件的區別:
    • Class.forName(className) 加載 class 的同時會初始化靜態域,ClassLoader#loadClass(className) 不會初始化靜態域;
    • Class.forName 藉助當前調用者的 class 的 ClassLoader 完成 class 的加載。

2、隱式加載

  • new 類對象;
  • 使用類的靜態域;
  • 創建子類對象;
  • 使用子類的靜態域;
  • 其他的隱式加載,在 JVM 啓動時:
    • BootStrapLoader 會加載一些 JVM 自身運行所需的 Class;
    • ExtClassLoader 會加載指定目錄下一些特殊的 Class;
    • AppClassLoader 會加載 classpath 路徑下的 Class,以及 main 函數所在的類的 Class 文件。

四、類加載的過程

1、加載

加載的過程:加載”是“類加載”過程的一個階段,不能混淆這兩個名詞。在加載階段,虛擬機需要完成 3 件事:

  • 通過類的全限定名獲取二進制字節流(將 .class 文件讀進內存);
  • 將字節流的靜態存儲結構轉化爲運行時的數據結構;
  • 在內存中生成該類的 Class 對象;HotSpot 虛擬機把這個對象放在方法區,非 Java 堆。

獲取二進制字節流:對於 Class 文件,虛擬機沒有指明要從哪裏獲取、怎樣獲取。除了直接從編譯好的 .class 文件中讀取,還有以下幾種方式:

  • 從 zip 包中讀取,如 jar、war等
  • 從網絡中獲取,如 Applect
  • 通過動態代理計數生成代理類的二進制字節流
  • 由 JSP 文件生成對應的 Class 類
  • 從數據庫中讀取,如 有些中間件服務器可以選擇把程序安裝到數據庫中來完成程序代碼在集羣間的分發。

“非數組類”與“數組類”加載比較:

  • 非數組類
    • 系統提供的引導類加載器
    • 用戶自定義的類加載器(如重寫一個類加載器的 loadClass() 方法)
  • 數組類
    • 不通過類加載器,由 Java 虛擬機直接創建
    • 創建動作由 newarray 指令觸發,new 實際上觸發了 [全類名] 對象的初始化
    • 規則
      • 數組元素是引用類型
        • 加載:遞歸加載其組件
        • 可見性:與引用類型一致
      • 數組元素是非引用類型
        • 加載:與引導類加載器關聯
        • 可見性:public

注意事項

  • 虛擬機規範未規定 Class 對象的存儲位置,對於 HotSpot 虛擬機而言,Class 對象比較特殊,它雖然是對象,但存放在方法區中。
  • 加載階段與連接階段的部分內容交叉進行,加載階段尚未完成,連接階段可能已經開始了。但這兩個階段的開始實踐仍然保持着固定的先後順序。

2、驗證

驗證的目的:驗證階段確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。

4 個驗證過程:

  • 文件格式驗證:是否符合 Class 文件格式規範,驗證文件開頭 4 個字節是不是 “魔數” 0xCAFEBABE
  • 元數據驗證:保證字節碼描述信息符號 Java 規範(語義分析)
  • 字節碼驗證:程序語義、邏輯是否正確(通過數據流、控制流分析)
  • 符號引用驗證:對類自身以外的信息(常量池中的符號引用)進行匹配性校驗

注意:這個操作雖然重要,但不是必要的,可以通過 -Xverify:none 關掉,可以縮短虛擬機類加載的時間

3、準備

描述:準備階段是正式爲類變量(或稱“靜態成員變量”)分配內存並設置初始值的階段。這些變量(不包括實例變量)所使用的內存都在方法區中進行分配。初始值“通常情況下”是數據類型的零值(0, null...)

 

靜態成員變量準備後的初始值:

  • public static int value = 123;
    • 準備後爲 0,value 的賦值指令 putstatic 會被放在 <client>() 方法中,<client>()方法會在初始化時執行,也就是說,value 變量只有在初始化後纔等於 123。
  • public static final int value = 123;
    • 準備後爲 123,因爲被 static final 賦值之後 value 就不能再修改了,所以在這裏進行了賦值之後,之後不可能再出現賦值操作,所以可以直接在準備階段就把 value 的值初始化好。

4、解析

描述: 將常量池中的 “符號引用” 替換爲 “直接引用”。

  • 在此之前,常量池中的引用是不一定存在的,解析過之後,可以保證常量池中的引用在內存中一定存在。
  • 什麼是 “符號引用” 和 “直接引用” ?
    • 符號引用:以一組符號描述所引用的對象(如對象的全類名),引用的目標不一定存在於內存中。
    • 直接引用:直接指向被引用目標在內存中的位置的指針等,也就是說,引用的目標一定存在於內存中。

5、初始化

描述: 類初始化階段是類加載過程的最後一步,是執行類構造器 <clinit>() 方法的過程。

<client>() 方法:

  • 包含的內容:
    • 所有 static 的賦值操作;
    • static 塊中的語句;
  • <client>() 方法中的語句順序:
    • 基本按照語句在源文件中出現的順序排列;
    • 靜態語句塊只能訪問定義在它前面的變量,定義在它後面的變量,可以賦值,但不能訪問。
  • 與 <init>() 的不同:
    • 不需要顯示調用父類的 <client>() 方法;
    • 虛擬機保證在子類的 <client>() 方法執行前,父類的 <client>() 方法一定執行完畢。
      • 也就是說,父類的 static 塊和 static 字段的賦值操作是要先於子類的。
  • 接口與類的不同:
    • 執行子接口的 <client>() 方法前不需要先執行父接口的 <client>() 方法(除非用到了父接口中定義的 public static final 變量);
  • 執行過程中加鎖:
    • 同一時刻只能有一個線程在執行 <client>() 方法,因爲虛擬機要保證在同一個類加載器下,一個類只被加載一次。
  • 非必要性:
    • 一個類如果沒有任何 static 的內容就不需要執行 <client>() 方法。

注:初始化時,才真正開始執行類中定義的 Java 代碼。

五、類加載器

1、如何判斷兩個類 “相等”

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

  • “相等” 的要求
    • 同一個 .class 文件
    • 被同一個虛擬機加載
    • 被同一個類加載器加載
  • 判斷 “相等” 的方法
    • instanceof 關鍵字
    • Class 對象中的方法:
      • equals()
      • isInstance()
      • isAssignableFrom()

2、類加載器的分類

類加載器
  • 啓動類加載器(Bootstrap ClassLoader): 負責將存放在 <JAVA_HOME>\lib 目錄中的,並且能被虛擬機識別的(僅按照文件名識別,如 rt.jar,名字不符合的類庫即使放在 lib 目錄中也不會被加載)類庫加載到虛擬機內存中。
  • 擴展類加載器(Extension ClassLoader): 負責加載 <JAVA_HOME>\lib\ext 目錄中的所有類庫,開發者可以直接使用擴展類加載器。
  • 應用程序類加載器(Application ClassLoader): 由於這個類加載器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以一般也稱它爲“系統類加載器”。它負責加載用戶類路徑(classpath)上所指定的類庫,開發者可以直接使用這個類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。

當然,如果有必要,還可以加入自己定義的類加載器。

3、雙親委派模型

雙親委派模型:雙親委派模型是描述類加載器之間的層次關係。它要求除了頂層的啓動類加載器外,其餘的類加載器都應當有自己的父類加載器。(父子關係一般不會以繼承的關係實現,而是以組合關係來複用父加載器的代碼)

工作過程:

  • 當前類加載器收到類加載的請求後,先不自己嘗試加載類,而是先將請求委派給父類加載器
    • 因此,所有的類加載請求,都會先被傳送到啓動類加載器
  • 只有當父類加載器加載失敗時,當前類加載器纔會嘗試自己去自己負責的區域加載

實現:

  • 檢查該類是否已經被加載
  • 將類加載請求委派給父類
    • 如果父類加載器爲 null,默認使用啓動類加載器
    • parent.loadClass(name, false)
  • 當父類加載器加載失敗時
    • catch ClassNotFoundException 但不做任何處理
    • 調用自己的 findClass() 去加載
      • 我們在實現自己的類加載器時只需要 extends ClassLoader,然後重寫 findClass() 方法而不是 loadClass() 方法,這樣就不用重寫 loadClass() 中的雙親委派機制了

優點:

像 java.lang.Object 這些存放在 rt.jar 中的類,無論使用哪個類加載器加載,最終都會委派給最頂端的啓動類加載器加載,從而使得不同加載器加載的 Object 類都是同一個。相反,如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶自己編寫了一個稱爲 java.lang.Object 的類,並放在 classpath 下,那麼系統將會出現多個不同的 Object 類,Java 類型體系中最基礎的行爲也就無法保證。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章