虛擬機類加載機制

整理自《深入理解 Java 虛擬機》。

1. 類加載時機

虛擬機把描述類的數據從 Class 文件加載到內存,並對數據進行校驗、轉換解析和初始化。最終形成可以被虛擬機直接使用的 Java 類型,就是虛擬機的類加載機制。一個類從被加載進內存,到卸載出內存,完整的生命週期包括:加載,驗證,準備,解析,初始化,使用,卸載。
在這裏插入圖片描述
加載、驗證、準備、初始化、卸載這5個步驟的順序是確定的,解析階段則不一定,某些時候可以在初始化之後,這是爲了支持Java的運行時綁定(動態綁定)。

什麼時候進行加載,虛擬機規範中沒有強制要求,但是初始化操作有嚴格的規定,5 種情況下必須立刻執行:

  • 遇到 new、getstatic、putstatic、invokestatic 這4個字節碼指令時,沒有初始化,必須初始化。典型場景:使用 new 關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被 final 修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,已及調用一個類的靜態方法的時候。
  • 使用 java.lang.reflect 包的方法對類進行反射調用的時候,如果類沒有初始化,則需要先觸發其初始化。
  • 初始化子類的時候,要先初始化其父類。
  • 虛擬機啓動時,要指定一個執行的主類(main方法的類),需要先初始化。
  • JDK 7的動態語言支持時,java.lang.invoke.MethodHandle 實例最後的解析結果是REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄時,對應的類沒有初始化,需要初始化。

2. 類加載的過程

2.1 加載

這個階段需要完成以下三件事:

  • 通過一個類的全限定名來獲取定義此類的二進制字節流。
  • 將這個二進制字節流轉儲爲方法區的運行時數據結構。
  • 於內存中生成一個代表這個類的 java.lang.Class 類型的對象,用於表示該類的類型信息,作爲方法區這個類的各種數據的訪問入口。

2.2 驗證

驗證階段的目的是爲了確保加載的 Class 文件中的字節流是符合虛擬機運行要求的,不能威脅到虛擬機自身安全。

這個階段直接決定了虛擬機能否承受住惡意代碼的攻擊。整個驗證又分爲四個階段:文件格式驗證、元數據驗證、字節碼驗證,符號引用驗證。

驗證階段不一定是必要的,對於信任的代碼可以考慮使用 Xverify:none 啓動參數關閉驗證階段,縮短虛擬機類加載時間。

2.3 準備

準備階段實際上是爲類變量(static 修飾的變量)分配內存(在方法區中)並賦「系統初值」的過程,這裏的「系統初值」並不是指通過賦值語句初始化變量的意思。

特殊情況:對於 public static final int value = 123; 編譯時Javac 將會爲 value 生成ConstantValue屬性,在準備階段虛擬機就會根據 ConstantValue 的設置將 value 賦值爲123。

2.4 解析

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

  • 符號引用(Symbolic References): 符號引用以一組符號來描述所引用的目標,符號可以是符合約定的任何形式的字面量,符號引用與虛擬機實現的內存佈局無關,引用的目標並不一定已經加載到內存中。
  • 直接引用(Direct References): 直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用與虛擬機實現的內存佈局相關,引用的目標必定已經在內存中存在。

2.5 初始化

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

  • < clint >() 方法是由編譯器自動收集類中的所有變量的賦值動作和靜態語句塊(static{} 塊)中的語句合併產生的。
  • 虛擬機會保證在子類的 < clint >() 方法執行之前,父類的 < clint >() 方法已經執行完畢。意味着父類中定義的靜態語句塊優先於子類的變量賦值操作。
  • < clint >() 不是必需的,如果一個類沒有靜態語句塊,也沒有變量的賦值操作,那麼編譯器可以不生成 < clint >() 方法。
  • 虛擬機保證一個類的 < clint >() 方法在多線程環境下被正確地加鎖、同步,只有一個線程會執行這個類的 < clint >() 方法。同一個類加載器下,一個類只會被初始化一次。

3. 類加載器

功能

把類加載階段的“通過一個類的全限定名來獲取描述此類的二進制字節流”這個動作交給虛擬機之外的類加載器來完成。這樣的好處在於,我們可以自行實現類加載器來加載其他格式的類,只要是二進制字節流就行。

對於任意一個類,都需要由它的類加載器和這個類本身一同確立其在 Java 虛擬機中的唯一性。

分類

從 Java 虛擬機角度,只存在兩種不同的類加載器:

  • 啓動類加載器(Bootstrap ClassLoader),由 C++ 實現,是虛擬機自身的一部分。
  • 其他類加載器,由 Java 語言實現,獨立於虛擬機外部,全部繼承自抽象類 java.lang.ClassLoader。

從 開發人員角度,分爲以下三種:

  • 啓動(Bootstrap)類加載器:它負責將 <JAVA_HOME>/lib下面的核心類庫或 -Xbootclasspath 選項指定的類庫等加載到內存中。引導類加載器是用本地代碼實現的類加載器,開發者無法直接獲取到啓動類加載器的引用,所以不允許直接通過引用進行操作。
  • 擴展(Extension)類加載器:擴展類加載器是由 sun.misc.Launcher$ExtClassLoader 實現的,它負責將 <JAVA_HOME >/lib/ext 目錄中的,或者由系統變量 java.ext.dir 指定位置中的類庫加載到內存中。開發者可以直接使用標準擴展類加載器。
  • 系統(System)類加載器:也叫應用程序類加載器,由 sun.misc.Launcher$AppClassLoader 實現的,它負責將用戶類路徑(ClassPath)下的類庫加載到內存中。開發者可以直接使用系統類加載器。如果應用程序中沒有自定義過自己的類加載器,這個就是默認的類加載器。

雙親委派模型

應用程序是由上述三種類加載器相互配合進行加載的,如有必要,還可以加入自己定義的類加載器。它們之間的關係:
在這裏插入圖片描述
雙親委派機制工作過程:

如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父加載器去完成,每個層次的類加載器都是如此,因此所有的加載請求最終都會傳送到頂層的啓動類加載器中。只有父類加載反饋自己無法加載這個請求(它的搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試自己去加載。

優點:
Java 類隨着它的加載器一起具備了一種帶有優先級的層次關係。通過這種層級關係很好地解決了基礎類的統一問題。例如類 java.lang.Object,它存放在 rt.jar 之中。無論哪一個類加載器要加載這個類,最終都是雙親委派模型最頂端的啓動類加載器去加載。因此 Object 類在程序的各種類加載器環境中都是同一個類。相反,如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶編寫了一個稱爲“java.lang.Object”的類,並存放在程序的 ClassPath 中,那系統中將會出現多個不同的Object 類,Java 類型體系中最基礎的行爲也就無法保證,應用程序也將會一片混亂。其次是考慮到安全因素,Java 核心 API 中定義類型不會被隨意替換,假設通過網絡傳遞一個名爲 java.lang.Integer 的類,通過雙親委託模式傳遞到啓動類加載器,而啓動類加載器在覈心 Java API 發現這個名字的類,發現該類已被加載,就不會重新加載網絡傳遞的過來的 java.lang.Integer,而直接返回已加載過的 Integer.class,這樣便可以防止核心 API 庫被隨意篡改。

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