JVM類(lèi)加載機(jī)制與雙親委派使用及說(shuō)明
一、JVM 四大類(lèi)加載器
JVM 的類(lèi)加載器(ClassLoader)負(fù)責(zé)將.class字節(jié)碼文件加載到內(nèi)存中,并生成對(duì)應(yīng)的Class對(duì)象。
Java 默認(rèn)提供了四層層級(jí)結(jié)構(gòu)的類(lèi)加載器,從上到下依次為:?jiǎn)?dòng)類(lèi)加載器、擴(kuò)展類(lèi)加載器、應(yīng)用程序類(lèi)加載器、自定義類(lèi)加載器。
1. 層級(jí)結(jié)構(gòu)與核心特性
| 類(lèi)加載器名稱(chēng) | 英文 / 簡(jiǎn)稱(chēng) | 加載范圍 | 實(shí)現(xiàn)方式 | 父加載器 |
|---|---|---|---|---|
| 啟動(dòng)類(lèi)加載器 | Bootstrap ClassLoader | JRE 核心類(lèi)庫(kù):rt.jar、resources.jar、sun.boot.class.path路徑下的類(lèi)(如java.lang.*、java.util.*) | C/C++ 實(shí)現(xiàn),JVM 內(nèi)置,無(wú)對(duì)應(yīng) Java 對(duì)象,無(wú)法在代碼中直接獲取 | 無(wú)(頂層加載器) |
擴(kuò)展類(lèi)加載器 (jdk9+后改名為平臺(tái)類(lèi)加載器) | Extension ClassLoader | JRE 擴(kuò)展目錄jre/lib/ext、java.ext.dirs指定路徑下的擴(kuò)展類(lèi) | Java 實(shí)現(xiàn),繼承自URLClassLoader | 啟動(dòng)類(lèi)加載器 |
| 應(yīng)用程序類(lèi)加載器 | Application ClassLoader(系統(tǒng)類(lèi)加載器) | 項(xiàng)目classpath下的自定義類(lèi)、第三方依賴(lài)包 | Java 實(shí)現(xiàn),繼承自URLClassLoader,是ClassLoader.getSystemClassLoader()的返回值 | 擴(kuò)展類(lèi)加載器 |
| 自定義類(lèi)加載器 | Custom ClassLoader | 自定義路徑的類(lèi)(網(wǎng)絡(luò)、加密字節(jié)碼、自定義目錄等) | 繼承ClassLoader重寫(xiě)核心方法實(shí)現(xiàn) | 默認(rèn)為應(yīng)用程序類(lèi)加載器 |
補(bǔ)充關(guān)鍵說(shuō)明
- 層級(jí)關(guān)系≠繼承關(guān)系:類(lèi)加載器的父子關(guān)系是組合引用(通過(guò)
parent字段關(guān)聯(lián)),不是 Java 類(lèi)的繼承; - 獲取系統(tǒng)類(lèi)加載器:代碼中調(diào)用
ClassLoader.getSystemClassLoader(),默認(rèn)獲取的就是應(yīng)用程序類(lèi)加載器; - Bootstrap 特殊點(diǎn):它是 JVM 本地代碼實(shí)現(xiàn)的,在 Java 中獲取它會(huì)返回
null。
2. 代碼驗(yàn)證類(lèi)加載器
通過(guò)簡(jiǎn)單代碼查看不同類(lèi)的加載器,直觀理解層級(jí)關(guān)系:
public class ClassLoaderDemo {
public static void main(String[] args) {
// 1. 核心類(lèi):由Bootstrap加載,輸出null
ClassLoader bootstrapLoader = String.class.getClassLoader();
System.out.println("String類(lèi)加載器: " + bootstrapLoader);
// 2. 系統(tǒng)類(lèi)加載器(應(yīng)用類(lèi)加載器)
ClassLoader appLoader = ClassLoaderDemo.class.getClassLoader();
System.out.println("自定義類(lèi)加載器: " + appLoader);
// 3. 應(yīng)用類(lèi)加載器的父加載器:擴(kuò)展類(lèi)加載器
ClassLoader extLoader = appLoader.getParent();
System.out.println("應(yīng)用類(lèi)加載器的父加載器: " + extLoader);
// 4. 擴(kuò)展類(lèi)加載器的父加載器:Bootstrap,輸出null
System.out.println("擴(kuò)展類(lèi)加載器的父加載器: " + extLoader.getParent());
}
}
二、雙親委派機(jī)制
1. 定義
雙親委派機(jī)制是 JVM 類(lèi)加載的默認(rèn)規(guī)則:當(dāng)一個(gè)類(lèi)加載器收到類(lèi)加載請(qǐng)求時(shí),不會(huì)自己先嘗試加載,而是將請(qǐng)求向上委托給父加載器,遞歸向上直到頂層啟動(dòng)類(lèi)加載器;只有當(dāng)父加載器無(wú)法加載該類(lèi)時(shí),子加載器才會(huì)嘗試自己加載。
2. 執(zhí)行流程(遞歸邏輯)
自定義類(lèi)加載器收到加載請(qǐng)求,先檢查是否已加載該類(lèi),已加載則直接返回Class對(duì)象;
未加載則委托給父加載器(應(yīng)用類(lèi)加載器);
應(yīng)用類(lèi)加載器重復(fù)檢查邏輯,委托給擴(kuò)展類(lèi)加載器;
擴(kuò)展類(lèi)加載器重復(fù)檢查邏輯,委托給啟動(dòng)類(lèi)加載器;
啟動(dòng)類(lèi)加載器嘗試在自身加載路徑查找類(lèi):
- 找到:直接加載并返回
Class對(duì)象; - 未找到:向下回溯,讓子加載器嘗試加載;
若所有層級(jí)加載器都無(wú)法加載,拋出ClassNotFoundException。
3. 核心優(yōu)勢(shì)
避免類(lèi)重復(fù)加載:保證同一個(gè)類(lèi)在 JVM 中只被加載一次,生成唯一的Class對(duì)象;
保障 Java 核心 API 安全(沙箱安全):防止惡意代碼自定義核心類(lèi)(如java.lang.String)替換 JDK 原生類(lèi),避免核心 API 被篡改;
層級(jí)管理類(lèi)資源:規(guī)范不同范圍類(lèi)的加載邊界,提升類(lèi)加載的穩(wěn)定性。
4. 源碼層面解析
雙親委派的核心邏輯封裝在ClassLoader的loadClass(String name, boolean resolve)方法中(JDK8 源碼核心邏輯):
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 檢查類(lèi)是否已經(jīng)被當(dāng)前加載器加載
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 2. 遞歸委托父加載器加載
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 3. 無(wú)父加載器,直接使用啟動(dòng)類(lèi)加載器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父加載器無(wú)法加載,捕獲異常
}
// 4. 父加載器加載失敗,當(dāng)前加載器自行加載
if (c == null) {
c = findClass(name);
}
}
// 解析類(lèi)
if (resolve) {
resolveClass(c);
}
return c;
}
}
關(guān)鍵方法:findClass()是子類(lèi)的擴(kuò)展點(diǎn),默認(rèn)拋出異常,自定義加載器需要重寫(xiě)它。
三、打破雙親委派機(jī)制
1. 為什么要打破?
默認(rèn)的雙親委派無(wú)法滿(mǎn)足所有場(chǎng)景,常見(jiàn)需求:
- 加載自定義路徑 / 特殊來(lái)源的類(lèi)(網(wǎng)絡(luò)字節(jié)碼、加密字節(jié)碼、模塊化 jar);
- 實(shí)現(xiàn)類(lèi)隔離(如 Web 容器 Tomcat、SPI 機(jī)制、熱部署);
- 同一個(gè)應(yīng)用中需要加載同一個(gè)類(lèi)的不同版本。
2. 打破的核心原理
雙親委派的邏輯寫(xiě)在loadClass()方法中,重寫(xiě)loadClass()方法,跳過(guò)向上委托的邏輯,就能直接打破該機(jī)制。
3. 標(biāo)準(zhǔn)實(shí)現(xiàn)方式:自定義類(lèi)加載器
步驟
- 繼承
java.lang.ClassLoader抽象類(lèi); - 重寫(xiě)
loadClass()方法,破壞默認(rèn)的雙親委派遞歸邏輯; - 重寫(xiě)
findClass()方法,實(shí)現(xiàn)自定義的類(lèi)字節(jié)碼讀取邏輯。
完整示例代碼
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
/**
* 打破雙親委派的自定義類(lèi)加載器
*/
public class CustomClassLoader extends ClassLoader {
// 自定義類(lèi)加載路徑
private final String classPath;
public CustomClassLoader(String classPath) {
// 關(guān)鍵:不指定父加載器(默認(rèn)父加載器為系統(tǒng)類(lèi)加載器,也可手動(dòng)設(shè)置)
this.classPath = classPath;
}
/**
* 重寫(xiě)loadClass,核心:跳過(guò)雙親委派的向上委托邏輯
*/
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 檢查是否已加載
Class<?> clazz = findLoadedClass(name);
if (clazz != null) {
return clazz;
}
// 2. 打破規(guī)則:核心類(lèi)仍交給Bootstrap加載,避免JDK核心類(lèi)加載異常
if (name.startsWith("java.")) {
return getSystemClassLoader().loadClass(name);
}
// 3. 不委托父加載器,直接自己加載
try {
clazz = findClass(name);
} catch (Exception e) {
// 加載失敗,降級(jí)為系統(tǒng)類(lèi)加載器加載
return super.loadClass(name, resolve);
}
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
}
/**
* 重寫(xiě)findClass:從自定義路徑讀取字節(jié)碼
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassBytes(name);
if (classData == null) {
throw new ClassNotFoundException("無(wú)法找到類(lèi):" + name);
}
// 將字節(jié)碼轉(zhuǎn)換為Class對(duì)象
return defineClass(name, classData, 0, classData.length);
}
/**
* 從文件系統(tǒng)讀取.class字節(jié)碼
*/
private byte[] getClassBytes(String className) {
String path = classPath + "/" + className.replace(".", "/") + ".class";
try (FileInputStream fis = new FileInputStream(path);
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
return bos.toByteArray();
} catch (IOException e) {
return null;
}
}
}
測(cè)試代碼
public class TestCustomLoader {
public static void main(String[] args) throws Exception {
// 自定義加載路徑
CustomClassLoader loader = new CustomClassLoader("D:/test/classes");
// 加載自定義類(lèi)
Class<?> clazz = loader.loadClass("com.test.DemoClass");
// 打印加載器,驗(yàn)證為自定義加載器
System.out.println("類(lèi)加載器: " + clazz.getClassLoader());
}
}
4. 行業(yè)中打破雙親委派的經(jīng)典場(chǎng)景
場(chǎng)景 1:Tomcat 等 Web 容器
- 需求:一個(gè) Web 服務(wù)器部署多個(gè)應(yīng)用,不同應(yīng)用可能依賴(lài)同一個(gè)類(lèi)的不同版本,需要類(lèi)隔離;
- 實(shí)現(xiàn):每個(gè) Web 應(yīng)用對(duì)應(yīng)一個(gè)獨(dú)立的
WebAppClassLoader,優(yōu)先加載應(yīng)用自身的類(lèi),跳過(guò)雙親委派向上委托邏輯,避免類(lèi)沖突。
場(chǎng)景 2:JDBC SPI 服務(wù)發(fā)現(xiàn)
- JDK 的
rt.jar中定義了java.sql.Driver接口(由 Bootstrap 加載),但具體驅(qū)動(dòng)實(shí)現(xiàn)類(lèi)(MySQL/PostgreSQL 驅(qū)動(dòng))在classpath下; - Bootstrap 加載器無(wú)法加載應(yīng)用層的驅(qū)動(dòng)類(lèi),因此通過(guò)
Thread.getContextClassLoader()(線程上下文類(lèi)加載器)打破委派,讓啟動(dòng)類(lèi)加載器反向使用應(yīng)用類(lèi)加載器加載實(shí)現(xiàn)類(lèi)。
場(chǎng)景 3:熱部署 / 熱加載
- 如 JRebel、Spring Boot DevTools,通過(guò)自定義類(lèi)加載器重新加載修改后的類(lèi),繞過(guò)默認(rèn)的緩存機(jī)制,實(shí)現(xiàn)代碼熱更新。
總結(jié)
核心知識(shí)點(diǎn)回顧:
- 四大類(lèi)加載器:Bootstrap(頂層、C 實(shí)現(xiàn))→ Extension → Application → 自定義加載器,層級(jí)為組合關(guān)系而非繼承;
- 雙親委派機(jī)制:向上委托、向下加載,核心價(jià)值是防重復(fù)加載、保障核心 API 安全,邏輯在
ClassLoader.loadClass()中; - 打破方式:重寫(xiě)
loadClass()方法跳過(guò)向上委托,配合自定義findClass()實(shí)現(xiàn)個(gè)性化加載; - 工業(yè)級(jí)場(chǎng)景:Tomcat 類(lèi)隔離、JDBC SPI、熱部署框架是打破雙親委派的典型應(yīng)用。
面試答題話術(shù)參考:
- JVM 有四層類(lèi)加載器,從頂層到底層分別是啟動(dòng)類(lèi)、擴(kuò)展類(lèi)、應(yīng)用類(lèi)和自定義類(lèi)加載器。
- 雙親委派是類(lèi)加載的默認(rèn)規(guī)則,請(qǐng)求會(huì)遞歸向上委托給父加載器,父加載器無(wú)法加載時(shí)子加載器才會(huì)處理,主要作用是避免類(lèi)重復(fù)和保證核心類(lèi)安全。
- 打破該機(jī)制的核心方法是自定義 ClassLoader 并重寫(xiě) loadClass 方法,跳過(guò)委托邏輯,像 Tomcat 的類(lèi)隔離、JDBC 的 SPI 機(jī)制都是實(shí)際業(yè)務(wù)中打破該機(jī)制的經(jīng)典場(chǎng)景。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
java實(shí)現(xiàn)html轉(zhuǎn)pdf方法步驟
這篇文章主要給大家介紹了關(guān)于java實(shí)現(xiàn)html轉(zhuǎn)pdf方法的相關(guān)資料,要將HTML轉(zhuǎn)換成PDF,我們需要借助Java中的第三方庫(kù),文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-08-08
java線程中synchronized和Lock區(qū)別及介紹
這篇文章主要為大家介紹了java線程中synchronized和Lock區(qū)別及介紹,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06
SpringBoot CountDownLatch多任務(wù)并行處理的實(shí)現(xiàn)方法
本篇文章主要介紹了SpringBoot CountDownLatch多任務(wù)并行處理的實(shí)現(xiàn)方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-04-04
springboot2.0使用Hikari連接池的方法(替換druid)
這篇文章主要介紹了springboot 2.0使用Hikari連接池的方法(替換druid),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12
Java使用Callable接口實(shí)現(xiàn)多線程的實(shí)例代碼
這篇文章主要介紹了Java使用Callable接口實(shí)現(xiàn)多線程的實(shí)例代碼,實(shí)現(xiàn)Callable和實(shí)現(xiàn)Runnable類(lèi)似,但是功能更強(qiáng)大,具體表現(xiàn)在可以在任務(wù)結(jié)束后提供一個(gè)返回值,Runnable不行,call方法可以拋出異,Runnable的run方法不行,需要的朋友可以參考下2023-08-08
idea導(dǎo)入springboot項(xiàng)目沒(méi)有maven的解決
這篇文章主要介紹了idea導(dǎo)入springboot項(xiàng)目沒(méi)有maven的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04
springboot集成JWT之雙重token的實(shí)現(xiàn)
本文主要介紹了springboot集成JWT之雙重token的實(shí)現(xiàn),前端使用accessToken進(jìn)行登錄和驗(yàn)證,后端使用refreshToken定期更新accessToken,具有一定的參考價(jià)值,感興趣的可以了解一下2025-03-03

