手機(jī)網(wǎng)站js特效個(gè)人博客登錄入口
序言
Java 語(yǔ)言的強(qiáng)大之處之一在于其動(dòng)態(tài)加載的能力,使得 Java 程序可以在運(yùn)行時(shí)加載新的類,而不需要在編譯時(shí)確定所有的類信息。這一切都離不開(kāi) JVM 的類加載機(jī)制。本篇博客將詳細(xì)探討 JVM 的類加載過(guò)程以及類加載器的工作原理,幫助你更深入地理解 Java 的動(dòng)態(tài)性和可擴(kuò)展性。
1. 類加載機(jī)制概述
在 Java 語(yǔ)言中,類的生命周期主要包括加載(Loading)、鏈接(Linking)和初始化(Initialization)三個(gè)階段。JVM 通過(guò)類加載器(ClassLoader)來(lái)完成這個(gè)過(guò)程,使 Java 具有動(dòng)態(tài)加載和模塊化的特性。
1.1 類的生命周期
一個(gè)類從被加載到 JVM,到最終被卸載,經(jīng)歷了如下幾個(gè)階段:
-
加載(Loading):JVM 通過(guò)類加載器讀取 class 文件,并生成
java.lang.Class
對(duì)象。 -
鏈接(Linking):
-
驗(yàn)證(Verification):確保 class 文件的字節(jié)碼符合 JVM 規(guī)范,保證安全性。
-
準(zhǔn)備(Preparation):為類的靜態(tài)變量分配內(nèi)存,并初始化默認(rèn)值(不包括賦值語(yǔ)句)。
-
解析(Resolution):將常量池中的符號(hào)引用解析為直接引用。
-
-
初始化(Initialization):執(zhí)行類的靜態(tài)初始化代碼,即
static
變量賦值和static
代碼塊。 -
使用(Using):類被實(shí)例化、調(diào)用方法等。
-
卸載(Unloading):當(dāng)類不再被引用時(shí),GC 可能會(huì)回收它(通常僅適用于自定義類加載器加載的類)。
2. 類加載過(guò)程詳解
類加載是 JVM 將字節(jié)碼數(shù)據(jù)從靜態(tài)存儲(chǔ)結(jié)構(gòu)(如 class 文件)轉(zhuǎn)換為運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)的過(guò)程。這個(gè)過(guò)程不僅包含簡(jiǎn)單的二進(jìn)制讀取,還需要完成復(fù)雜的校驗(yàn)、內(nèi)存分配和符號(hào)解析等操作。下面將詳細(xì)解析每個(gè)階段的實(shí)現(xiàn)細(xì)節(jié)。
2.1 加載(Loading)
加載階段是類加載的第一個(gè)環(huán)節(jié),核心任務(wù)是通過(guò)全限定名(如 java.lang.String
)獲取類的二進(jìn)制字節(jié)流,并將其轉(zhuǎn)換為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu),最終在堆中生成一個(gè) java.lang.Class
對(duì)象作為訪問(wèn)入口。
關(guān)鍵步驟
-
獲取二進(jìn)制字節(jié)流:類加載器通過(guò)全限定名定位資源,可能來(lái)自文件系統(tǒng)、JAR 包、網(wǎng)絡(luò)或動(dòng)態(tài)生成(如動(dòng)態(tài)代理)。
-
解析為方法區(qū)結(jié)構(gòu):將字節(jié)流代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
-
創(chuàng)建 Class 對(duì)象:在堆中創(chuàng)建該類的
Class
對(duì)象,作為程序訪問(wèn)方法區(qū)數(shù)據(jù)的接口。
觸發(fā)條件
-
首次創(chuàng)建類的實(shí)例(
new
關(guān)鍵字)。 -
訪問(wèn)類的靜態(tài)變量或靜態(tài)方法。
-
通過(guò)反射(如
Class.forName()
)顯式加載類。 -
子類初始化時(shí)觸發(fā)父類的加載。
2.2 鏈接(Linking)
鏈接階段分為三個(gè)子階段:驗(yàn)證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)。
2.2.1 驗(yàn)證(Verification)
驗(yàn)證確保字節(jié)碼符合 JVM 規(guī)范且不會(huì)危害虛擬機(jī)安全,包含以下四個(gè)子階段:
-
文件格式驗(yàn)證
-
檢查魔數(shù)(
0xCAFEBABE
)是否合法,確認(rèn)是否為有效的 class 文件。 -
檢查主次版本號(hào)是否在當(dāng)前 JVM 支持范圍內(nèi)。
-
檢查常量池中的常量是否有不被支持的類型。
-
-
元數(shù)據(jù)驗(yàn)證
-
檢查類是否有父類(除
java.lang.Object
外所有類必須有父類)。 -
檢查父類是否被 final 修飾(若被 final 修飾則不能繼承)。
-
確保字段、方法是否與父類沖突(如覆蓋 final 方法)。
-
-
字節(jié)碼驗(yàn)證
-
確保操作數(shù)棧的數(shù)據(jù)類型與指令兼容(如不會(huì)出現(xiàn) int 入棧卻按 long 處理)。
-
檢查跳轉(zhuǎn)指令是否指向合法位置(如不會(huì)跳轉(zhuǎn)到方法體外的字節(jié)碼)。
-
-
符號(hào)引用驗(yàn)證
-
檢查符號(hào)引用能否找到對(duì)應(yīng)的類、方法或字段。
-
確保訪問(wèn)權(quán)限合法(如 private 方法是否被外部類訪問(wèn))。
-
2.2.2 準(zhǔn)備(Preparation)
準(zhǔn)備階段為類的靜態(tài)變量分配內(nèi)存并設(shè)置初始值(零值):
-
基本類型:
int
初始化為0
,boolean
初始化為false
。 -
引用類型:初始化為
null
。 -
例外:若靜態(tài)變量被
final
修飾且在編譯期已知(如static final int VALUE = 123
),則直接賦值為指定值。
2.2.3 解析(Resolution)
解析階段將常量池中的符號(hào)引用替換為直接引用(內(nèi)存地址偏移量或句柄):
-
類/接口解析:若符號(hào)引用指向類,需先完成該類的加載。
-
字段解析:解析字段所屬的類,并驗(yàn)證是否存在及權(quán)限。
-
方法解析:與方法表關(guān)聯(lián),檢查方法是否存在及權(quán)限。
-
接口方法解析:類似方法解析,但需考慮接口的多繼承特性。
注意:解析階段可能在初始化之后觸發(fā)(如動(dòng)態(tài)綁定或 invokedynamic 指令)。
2.3 初始化(Initialization)
初始化階段執(zhí)行類的構(gòu)造器 <clinit>()
方法,該方法由編譯器自動(dòng)生成,合并所有靜態(tài)變量的賦值語(yǔ)句和靜態(tài)代碼塊。
關(guān)鍵特性
-
線程安全:JVM 通過(guò)加鎖確保
<clinit>()
只執(zhí)行一次。 -
順序性:父類的
<clinit>()
優(yōu)先于子類執(zhí)行。 -
主動(dòng)觸發(fā):只有以下情況會(huì)觸發(fā)初始化(加載和鏈接可能提前完成):
-
new
、getstatic
、putstatic
、invokestatic
指令。 -
反射調(diào)用類時(shí)(如
Class.forName("MyClass")
)。 -
主類(包含
main()
方法的類)在啟動(dòng)時(shí)立即初始化。
-
示例
public class Example {static int a = 1; ? ? ? ? // 準(zhǔn)備階段 a=0 → 初始化階段 a=1 ?static final int b = 2; ? // 準(zhǔn)備階段直接賦值 b=2 ?static {System.out.println("Static block executed.");} }
2.4 使用(Using)與卸載(Unloading)
-
使用階段:類完成初始化后,可被用于創(chuàng)建對(duì)象、調(diào)用方法等。
-
卸載條件:
-
類的所有實(shí)例已被回收。
-
類的
Class
對(duì)象未被引用(如無(wú)反射持有)。 -
加載該類的
ClassLoader
實(shí)例已被回收。
注意:由啟動(dòng)類加載器(Bootstrap)加載的類通常不會(huì)被卸載。
-
3. JVM 類加載器(ClassLoader)
類加載器是 Java 實(shí)現(xiàn)動(dòng)態(tài)加載的關(guān)鍵組件。它的主要作用是負(fù)責(zé)查找和加載類的字節(jié)碼,并定義類對(duì)象。
3.1 類加載器的層級(jí)結(jié)構(gòu)
JVM 的類加載器是分層委托模型(雙親委派機(jī)制),主要包括以下幾種類加載器:
-
Bootstrap ClassLoader(啟動(dòng)類加載器)
-
負(fù)責(zé)加載
JAVA_HOME/lib
目錄下的rt.jar
(Java 核心類庫(kù),如java.lang.*
)。 -
由 C/C++ 語(yǔ)言實(shí)現(xiàn),無(wú)法在 Java 代碼中獲取它的實(shí)例。
-
-
ExtClassLoader(擴(kuò)展類加載器)
-
負(fù)責(zé)加載
JAVA_HOME/lib/ext/
目錄中的擴(kuò)展類。 -
可通過(guò)
ClassLoader.getSystemClassLoader().getParent()
獲取。
-
-
AppClassLoader(應(yīng)用類加載器)
-
負(fù)責(zé)加載應(yīng)用程序的
classpath
目錄下的類。 -
ClassLoader.getSystemClassLoader()
返回的就是它。
-
-
自定義類加載器(用戶自定義 ClassLoader)
-
通過(guò)繼承
ClassLoader
,可實(shí)現(xiàn)自定義的類加載邏輯。 -
適用于類熱替換、動(dòng)態(tài)插件、代碼加密等場(chǎng)景。
-
類加載器層次結(jié)構(gòu):
Bootstrap ClassLoader (引導(dǎo)類加載器,加載 Java 核心類庫(kù))↓ ExtClassLoader (擴(kuò)展類加載器,加載 ext 目錄下的類)↓ AppClassLoader (應(yīng)用類加載器,加載 classpath 下的類)↓ CustomClassLoader (自定義類加載器)
4. 雙親委派模型(Parent Delegation Model)
4.1 什么是雙親委派機(jī)制?
JVM 采用雙親委派機(jī)制來(lái)加載類,即先交給父類加載器加載,如果父類加載器找不到該類,才由當(dāng)前類加載器加載。這個(gè)機(jī)制的流程如下:
-
一個(gè)類加載請(qǐng)求首先被交給父加載器處理。
-
如果父加載器找不到(即未加載過(guò)該類),則交給子加載器加載。
-
如果所有父加載器都找不到,才由當(dāng)前類加載器嘗試加載該類。
4.2 為什么要使用雙親委派?
-
避免類重復(fù)加載:保證 Java 核心類庫(kù)不會(huì)被重復(fù)定義。
-
提高安全性:防止核心 API(如
java.lang.String
)被惡意篡改。 -
減少類加載沖突:確保應(yīng)用類可以安全地使用 JDK 核心類庫(kù)。
4.3 破壞雙親委派的情況
某些場(chǎng)景下,開(kāi)發(fā)者可能會(huì)打破雙親委派機(jī)制,例如:
-
OSGi 模塊化系統(tǒng):不同模塊可能需要加載相同類的不同版本。
-
熱部署框架(Tomcat、Spring Boot):支持動(dòng)態(tài)替換類,如 Web 容器的
WebAppClassLoader
。 -
自定義類加載器:用于加密解密、動(dòng)態(tài)代理等場(chǎng)景。
5. 自定義類加載器示例
自定義 ClassLoader
需要繼承 ClassLoader
并重寫(xiě) findClass()
方法:
import java.io.*; ? public class MyClassLoader extends ClassLoader {private String classPath; ?public MyClassLoader(String classPath) {this.classPath = classPath;} ?@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {byte[] data = loadClassData(name);return defineClass(name, data, 0, data.length);} ?private byte[] loadClassData(String name) throws ClassNotFoundException {try {String fileName = classPath + name.replace(".", "/") + ".class";InputStream input = new FileInputStream(fileName);ByteArrayOutputStream output = new ByteArrayOutputStream();int ch;while ((ch = input.read()) != -1) {output.write(ch);}input.close();return output.toByteArray();} catch (IOException e) {throw new ClassNotFoundException(name);}} ?public static void main(String[] args) throws Exception {MyClassLoader loader = new MyClassLoader("path/to/classes/");Class<?> clazz = loader.loadClass("com.example.MyClass");Object obj = clazz.getDeclaredConstructor().newInstance();System.out.println(obj.getClass().getName());} }
總結(jié)
本篇博客詳細(xì)介紹了 JVM 的類加載機(jī)制,包括類的生命周期、類加載器的層級(jí)結(jié)構(gòu)、雙親委派模型及其應(yīng)用。理解這些內(nèi)容,有助于我們優(yōu)化 Java 應(yīng)用程序,避免類加載沖突,并實(shí)現(xiàn)一些高級(jí)特性(如插件化、動(dòng)態(tài)代理等)。