【JVM】Java類加載機制
什么是類加載?
在 Java 的世界里,每一個類或接口在經過編譯后,都會生成對應的 .class
字節碼文件。
所謂類加載機制,就是 JVM 將這些 .class
文件中的二進制數據加載到內存中,并對其進行校驗、解析、初始化等一系列操作。最終,每個類都會在方法區(或元空間)中保留一份結構化的類信息(元數據),并在Java 堆中創建一個 java.lang.Class
類型的對象,供程序運行時使用。
從 JVM 的角度看,一個類的生命周期包括以下 7 個階段:
加載 → 驗證 → 準備 → 解析 → 初始化 → 使用 → 卸載
其中,前五個階段(加載、驗證、準備、解析、初始化)統稱為類的加載過程。其中,驗證、準備和解析這三個階段可以統稱為連接。
需要注意的是,類加載的五個階段并不是嚴格按順序線性執行的,而是相互交叉、動態混合的過程。
例如:
- 部分驗證操作(如文件格式驗證)可能在加載
.class
文件的過程中就被觸發。- 符號引用的解析可能被延遲到真正使用時才發生。
類加載的詳細過程
1.加載
-
任務: 查找并加載類的二進制數據(通常是
.class
文件,但來源可以多樣)。 -
過程:
- 通過類的全限定名(如
java.lang.String
或com.example.MyClass
)獲取定義此類的二進制字節流。這個字節流來源可以是文件系統(最常見的)、網絡、ZIP/JAR包、運行時計算生成(動態代理)、數據庫等等。
類的全限定名就是類的完整名稱,即:包名 + 類名(如
java.lang.String
或com.example.MyClass
)- 將這個字節流所代表的靜態存儲結構轉換為方法區 的運行時數據結構。
- 在堆(Heap) 內存中創建一個代表這個類的
java.lang.Class
對象,作為方法區中這個類的各種數據的訪問入口。常用的.class
和getClass()
返回的就是這個對象。
public class CustomLoader extends ClassLoader {@Overrideprotected Class<?> findClass(String name) {// 1. 獲取字節流(可來自文件/網絡/數據庫等)byte[] bytes = loadClassBytes(name); // 2. 轉換為方法區數據結構// 3. 創建Class對象return defineClass(name, bytes, 0, bytes.length);} }
- 通過類的全限定名(如
-
關鍵點:
-
由類加載器 完成。
-
加載的最終產物是堆中的
Class
對象。 -
類的加載是懶惰的,首次用到時才會加載:
- 使用了類.class
- 用類加載器的 loadClass 方法加載類
- 滿足類的初始化條件(后文有詳細介紹)
參考:2-類加載_驗證類加載是懶惰的_嗶哩嗶哩_bilibili
-
2.連接
2.1 驗證
- 任務: 確保加載的字節碼信息符合 JVM 規范,是安全、無害的,不會危害虛擬機自身安全。
- 內容:
- 文件格式驗證(魔數、版本號等)
- 元數據驗證(語義分析,如是否有父類、是否繼承了final類、是否實現接口所有方法等)
- 字節碼驗證(數據流和控制流分析,確保邏輯正確,如操作數棧類型匹配、跳轉指令目標合理等)
- 符號引用驗證(檢查常量池中的符號引用能否找到對應的類、字段、方法等)。
- 重要性: 保護JVM安全,防止惡意代碼或損壞的字節碼文件導致JVM崩潰或執行危險操作。雖然耗點時間,但對系統穩定性至關重要。
2.2 準備
- 任務: 為類的靜態變量分配內存,并設置默認初始值。
- 過程:
- 在方法區中為這些靜態變量分配內存空間。
- 設置默認初始值:
- 基本類型:
int
->0
,long
->0L
,float
->0.0f
,double
->0.0d
,char
->'\u0000'
,boolean
->false
。 - 引用類型:
null
。
- 基本類型:
- 關鍵點:
- 這里分配內存并初始化的是類變量(static變量),不是實例變量。
- 初始化的值是默認零值,不是代碼中顯式賦的值(如
public static int value = 123;
,在準備階段后value
是0
,賦值123
的操作發生在后面的初始化階段)。 - 如果靜態變量是
final
修飾的基本類型或 String 常量,并且在編譯時就能確定值(如public static final int CONSTANT = 100;
),那么這個值會直接在準備階段被賦予(此時CONSTANT
就是100
)。
2.3 解析
- 任務: 將常量池內的符號引用 替換為直接引用。
- 符號引用與直接引用:
- 符號引用: 一組描述被引用目標(類、字段、方法)的符號。例如,
java/lang/Object
(類名)、toString:()Ljava/lang/String;
(方法名和描述符)。它只是一個字面量引用,與內存布局無關。 - 直接引用: 一個能直接定位到目標(類在方法區的地址、字段或方法在內存中的偏移量或句柄)的指針、偏移量或句柄。它是與JVM運行時內存布局相關的。
- 符號引用: 一組描述被引用目標(類、字段、方法)的符號。例如,
- 過程: JVM 查找符號引用所指向的類、字段或方法的實際位置,并將常量池中的符號引用替換為指向該位置的直接引用。
- 時機: 解析階段可以在初始化之前完成,也可以在初始化之后完成(甚至延遲到第一次實際使用該符號引用時),這取決于 JVM 的實現策略(“及早解析”或“惰性解析”)。
3.初始化
- 任務: 執行類的初始化代碼,主要是執行類構造器
<clinit>()
方法。 <clinit>()
方法:- 由編譯器自動收集類中所有類變量(static變量)的顯式賦值動作和靜態代碼塊(static {} 塊) 中的語句合并生成。
- 順序:按源代碼中出現的順序執行。
- 父類的
<clinit>()
優先于子類的執行。 - 虛擬機會保證一個類的
<clinit>()
方法在多線程環境下被正確地加鎖、同步(即線程安全)。如果一個線程正在執行它,其他線程會阻塞等待。
- 觸發時機(嚴格規定): 類只有在以下 6 種情況之一發生時,才會立即進行初始化(加載和連接可能更早發生):
- 創建類的實例 (
new
)。 - 訪問類的靜態變量(讀取或賦值),除非該靜態變量是
final
常量并且在編譯期就能確定值(常量傳播優化)。 - 調用類的靜態方法 (
static
方法)。 - 使用反射 (
Class.forName("...")
,getMethod
等) 對類進行反射調用。 - 初始化一個類的子類時,會觸發其父類的初始化。
- JVM 啟動時被標明為啟動類(包含
main()
方法的那個類)。
- 創建類的實例 (
- 關鍵點:
- 這是類加載過程的最后一步。
- 此時才真正執行程序員的代碼邏輯(靜態賦值、靜態塊)。
- 之前的“準備”階段只是分配內存并賦零值,這里是賦程序員定義的值。
類加載器
在類加載的第一個階段——加載中,JVM 需要根據類的全限定名,找到并讀取其對應的字節碼文件(.class
文件)。
這個查找和讀取 .class
字節流的工作,正是由類加載器來完成的。
JVM 中內置了三個重要的 ClassLoader
:
- Bootstrap ClassLoader (啟動類/引導類加載器):
- 用原生代碼(C/C++)實現,是 JVM 自身的一部分。
- 負責加載
JAVA_HOME/lib
目錄下的核心 Java 庫(如rt.jar
,charsets.jar
)或-Xbootclasspath
參數指定的路徑中的類。 - 是最高級別的加載器,沒有父加載器。
null
表示: 在 Java 代碼中試圖獲取它的引用時,返回null
。
- Extension ClassLoader (擴展類加載器):
- 由
sun.misc.Launcher$ExtClassLoader
實現(Java)。 - 負責加載
JAVA_HOME/lib/ext
目錄下的擴展庫,或java.ext.dirs
系統變量指定的路徑中的所有類庫。 - 其父加載器是 Bootstrap ClassLoader。
- 由
- Application ClassLoader (應用程序類加載器 / 系統類加載器):
- 由
sun.misc.Launcher$AppClassLoader
實現(Java)。 - 負責加載用戶類路徑(ClassPath) 上所指定的類庫。這是我們程序中默認的類加載器。
- 其父加載器是 Extension ClassLoader。
- 通過
ClassLoader.getSystemClassLoader()
可以獲取到它。
- 由
除了這三種類加載器之外,用戶還可以加入自定義的類加載器來進行拓展,以滿足自己的特殊需求:
除了 BootstrapClassLoader
是 JVM 自身的一部分之外,其他所有的類加載器都是在 JVM 外部實現的,并且全都繼承自 ClassLoader
抽象類。如果我們要自定義自己的類加載器,需要繼承 ClassLoader
抽象類。
ClassLoader
類中有兩個核心方法:
-
protected Class<?> loadClass(String name, boolean resolve)
加載指定名稱的類。該方法實現了雙親委派模型:會先委托給父加載器嘗試加載,如果父加載器無法完成,才會調用自身的
findClass()
方法進行加載。protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {// 先檢查當前類是否已經加載Class<?> c = findLoadedClass(name);if (c == null) {try {if (parent != null) {// 先讓父類加載器嘗試加載c = parent.loadClass(name, false);} else {// 如果沒有父加載器(即引導類加載器),使用 bootstrap 加載c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// 忽略異常,進入下一步由自己加載}if (c == null) {// 如果父加載器無法加載,再嘗試使用當前類加載器加載c = findClass(name);}}if (resolve) {resolveClass(c);}return c;} }
-
protected Class<?> findClass(String name)
根據類名查找類的定義并返回對應的Class
對象。默認實現是拋出ClassNotFoundException
,需要我們在子類中重寫。protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name); }
如果我們不想打破雙親委派模型,就重寫 ClassLoader
類中的 findClass()
方法即可,無法被父類加載器加載的類最終會通過這個方法被加載。但是,如果想打破雙親委派模型則需要重寫 loadClass()
方法。
雙親委派模型簡介
雙親委派模型是 Java 類加載機制的重要組成部分,它通過委派父加載器優先加載類的方式,實現了兩個關鍵的安全目標:避免類的重復加載和防止核心 API 被篡改。
-
工作流程: 當一個類加載器收到加載類的請求時:
- 它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成。
- 每一層的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器。
- 只有當父加載器反饋自己無法完成這個加載請求(在它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。
-
核心思想: “向上委派,向下加載”。
-
核心目的:
- 保證基礎類的唯一性和安全性: 防止用戶自定義一個與核心庫(如
java.lang.Object
)同名的類被加載,從而覆蓋核心庫的行為(沙箱安全機制)。 - 避免重復加載: 父加載器已經加載過的類,子加載器就不會再加載(在同一個命名空間內)。
- 保證基礎類的唯一性和安全性: 防止用戶自定義一個與核心庫(如
內容參考
【JVM】Java類加載機制這塊算是玩明白了_嗶哩嗶哩_bilibili
類加載器詳解(重點)