JVM 類(lèi)加載器有哪些?雙親委派機(jī)制的作用是什么?如何自定義類(lèi)加載器?
類(lèi)加載器分類(lèi)
先回顧下,在 Java 中,類(lèi)的初始化分為幾個(gè)階段: 加載、鏈接(包括驗(yàn)證、準(zhǔn)備和解析)和 初始化。
而 類(lèi)加載器(Class Loader)則是加載階段中,負(fù)責(zé)將本地或網(wǎng)絡(luò)中的指定類(lèi)的二進(jìn)制流,加載到 Java 虛擬機(jī)中的工具。
圖片
引導(dǎo)類(lèi)加載器 BootstrapClassLoader
引導(dǎo)類(lèi)加載器 BootstrapClassLoader:引導(dǎo)類(lèi)加載器是使用 C++ 語(yǔ)言實(shí)現(xiàn)的,嵌入在 JVM 中。用于加載 Java 中的核心類(lèi)庫(kù)的,不繼承自 java.lang.ClassLoader,在 Java 程序中通常返回 null。
一般會(huì)加載 JAVA_HOME 目錄下的 /jre/lib 文件夾下的 jar 和配置。
ClassLoader loader = String.class.getClassLoader();
System.out.println(loader); // 輸出 null,因?yàn)?String 是由引導(dǎo)類(lèi)加載器加載的
擴(kuò)展類(lèi)加載器 ExtClassLoader
擴(kuò)展類(lèi)加載器主要負(fù)責(zé)加載 Java 的擴(kuò)展類(lèi)庫(kù),一般會(huì)加載 JAVA_HOME 目錄下的 /jre/lib/ext 文件夾下的 jar。
繼承自 java.lang.ClassLoader,是用戶(hù)可以訪問(wèn)的第一個(gè)類(lèi)加載器。
ClassLoader extLoader = ClassLoader.getSystemClassLoader().getParent();
System.out.println(extLoader); // 輸出 sun.misc.Launcher$ExtClassLoader
應(yīng)用類(lèi)加載器(Application ClassLoader)
應(yīng)用類(lèi)加載器是應(yīng)用程序中默認(rèn)的類(lèi)加載器,可以加載 CLASSPATH 變量指定目錄下的 jar,由 sun.misc.Launcher$AppClassLoader 實(shí)現(xiàn)。
并且一般情況下,我們編寫(xiě)的 Java 應(yīng)用的類(lèi),都是使用該類(lèi)加載器完成加載的。
ClassLoader appLoader = ClassLoader.getSystemClassLoader();
System.out.println(appLoader); // 輸出 sun.misc.Launcher$AppClassLoader
類(lèi)加載器抽象類(lèi) ClassLoader
在 Java 中存在一個(gè)類(lèi)加載器抽象類(lèi) ClassLoader,大多數(shù)類(lèi)加載器都是通過(guò)繼承這個(gè)類(lèi)來(lái)實(shí)現(xiàn)的類(lèi)加載功能。以下是 ClassLoader 類(lèi)的關(guān)鍵部分代碼:
public abstract class ClassLoader {
/*
* 類(lèi)加載器的父加載器
*/
private final ClassLoader parent;
/**
* 根據(jù)類(lèi)的全限定名加載類(lèi)
*
* @param name 類(lèi)名稱(chēng)
* @return 加載的Class對(duì)象
* @throws ClassNotFoundException 沒(méi)有發(fā)現(xiàn)指定類(lèi)異常
*/
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 調(diào)用loadClass方法加載類(lèi),其中設(shè)置resolve=false,表示不立即解析類(lèi)
return loadClass(name, false);
}
/**
* 根據(jù)類(lèi)的全限定名加載類(lèi)
*
* @param name 類(lèi)名稱(chēng)
* @param resolve 是否解析這個(gè)類(lèi),true=解析,false=不解析
* @return 加載的Class對(duì)象
* @throws ClassNotFoundException 沒(méi)有發(fā)現(xiàn)指定類(lèi)異常
*/
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 檢查類(lèi)是否已經(jīng)被加載
Class<?> c = findLoadedClass(name);
// 如果沒(méi)有加載過(guò)
if (c == null) {
// 如果有父類(lèi)加載器,則委托給父加載器去加載
// 如果沒(méi)有父類(lèi)加載器,則判斷 Bootstrap 類(lèi)加載器是否加載過(guò)
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
// 如果父類(lèi)加載器都加載失敗,則當(dāng)前類(lèi)加載器嘗試自行加載
if (c == null) {
c = findClass(name);
}
}
// 據(jù) resolve 參數(shù)決定是否解析類(lèi)
if (resolve) {
resolveClass(c);
}
return c;
}
}
/**
* 查找并加載指定名稱(chēng)的類(lèi)
*
* @param name 類(lèi)名稱(chēng)
* @return Class對(duì)象
* @throws ClassNotFoundException 沒(méi)有發(fā)現(xiàn)指定類(lèi)異常
*/
protected Class<?> findClass(String name) throws ClassNotFoundException {
//1. 根據(jù)傳入的類(lèi)名,到在特定目錄下去尋找類(lèi)文件,把字節(jié)碼文件讀入內(nèi)存
// ...
//2. 調(diào)用 defineClass 將字節(jié)數(shù)組轉(zhuǎn)成 Class 對(duì)象
return defineClass(buf, off, len);
}
/**
* 將一個(gè) byte[] 轉(zhuǎn)換為 Class 類(lèi)的實(shí)例
*
* @param name 類(lèi)名稱(chēng),如果不知道此名稱(chēng),則該參數(shù)為 null
* @param b 組成類(lèi)數(shù)據(jù)的字節(jié)數(shù)組
* @param off 類(lèi)數(shù)據(jù)的起始偏移量
* @param len 類(lèi)數(shù)據(jù)的長(zhǎng)度
* @return Class對(duì)象
* @throws ClassFormatError 類(lèi)格式化異常
*/
protected final Class<?> defineClass(byte[] b, int off, int len) throws ClassFormatError {
...
}
}
類(lèi)中定義的常用的類(lèi)加載相關(guān)的方法:
方法名稱(chēng) | 描述 |
getParent() | 返回該類(lèi)加載器的父類(lèi)加載器 |
loadClass(String name) | 加載指定名稱(chēng)的類(lèi),返回 java.lang.Class 實(shí)例 |
findClass(String name) | 查找指定名稱(chēng)的類(lèi),返回 java.lang.Class 實(shí)例 |
findLoadedClass(String name) | 查找已加載的指定名稱(chēng)的類(lèi),返回 java.lang.Class 實(shí)例 |
defineClass(String name, byte[] b, int off, int len) | 將字節(jié)數(shù)組轉(zhuǎn)換為一個(gè) Java 類(lèi),返回 java.lang.Class 實(shí)例 |
resolveClass(Class c) | 連接指定的 Java 類(lèi) |
雙親委派模型(Parent Delegation Model)
雙親委派模型 是類(lèi)加載器的設(shè)計(jì)模式,其核心思想是:類(lèi)加載請(qǐng)求由子類(lèi)加載器向父類(lèi)加載器逐層委派,直到引導(dǎo)類(lèi)加載器。
如果父類(lèi)加載器無(wú)法加載,子類(lèi)加載器才會(huì)嘗試加載。
如果子類(lèi)加載器也無(wú)法加載該類(lèi),就會(huì)拋出一個(gè) ClassNotFoundException 異常。
圖片
雙親委派機(jī)制的作用
我們?cè)囅胍幌?,如果不使用這種委托模式,那我們就可以隨時(shí)使用自定義的 String 類(lèi)來(lái)動(dòng)態(tài)替代 Java 核心 API 中定義的類(lèi)型,這樣會(huì)存在非常大的安全隱患。
而雙親委托的方式,就可以避免這種情況,因?yàn)?String 已經(jīng)在啟動(dòng)時(shí)就被引導(dǎo)類(lèi)加載器 (BootstrcpClassLoader) 加載,所以用戶(hù)自定義的 ClassLoader 永遠(yuǎn)也無(wú)法加載一個(gè)用戶(hù)自己自定義的 String 類(lèi),除非你改變 JDK 中 ClassLoader 搜索類(lèi)的默認(rèn)算法。
該機(jī)制的作用如下。
- 防止重復(fù)加載字節(jié)碼文件: 將類(lèi)加載請(qǐng)求先委托給父類(lèi),父類(lèi)加載后子類(lèi)就不會(huì)重復(fù)加載該類(lèi)。所以,雙親委派機(jī)制可以防止對(duì)某個(gè)類(lèi)重復(fù)加載;
- 防止核心字節(jié)碼文件被篡改: 一般情況下引導(dǎo)類(lèi)加載器會(huì)先加載 JVM 核心類(lèi)庫(kù),然后其它加載器才會(huì)執(zhí)行,如果其它加載器要加載一個(gè)被篡改的核心字節(jié)碼文件,會(huì)將該文件委托給父類(lèi)加載器,當(dāng)委托到引導(dǎo)類(lèi)加載器時(shí),加載器已經(jīng)加載過(guò)該類(lèi),就不會(huì)對(duì)該類(lèi)進(jìn)行重復(fù)加載。而且就算能被加載,那么加載它的肯定不是相同的類(lèi)加載器 (不會(huì)是引導(dǎo)類(lèi)加載器),Java 虛擬機(jī)中只認(rèn)可核心類(lèi)加載器加載的核心類(lèi)庫(kù),所以,雙親委派機(jī)制可以防止核心字節(jié)碼文件被篡改。
- 簡(jiǎn)化加載邏輯: 通過(guò)委派模式,每個(gè)類(lèi)加載器只需要關(guān)注自己負(fù)責(zé)的那部分類(lèi)加載邏輯,而不必關(guān)心其他類(lèi)加載器的加載細(xì)節(jié),簡(jiǎn)化了類(lèi)加載器的實(shí)現(xiàn),降低了系統(tǒng)的復(fù)雜度。
自定義類(lèi)加載器
在某些場(chǎng)景下,標(biāo)準(zhǔn)的類(lèi)加載器無(wú)法滿(mǎn)足需求,例如:
- 熱部署:在 Web 服務(wù)器中動(dòng)態(tài)加載或更新類(lèi)。
- 模塊隔離:在同一個(gè) JVM 中加載不同版本的類(lèi)。
- 加密解密:加載經(jīng)過(guò)加密的 Class 文件。
默認(rèn)的類(lèi)加載器只能加載指定目錄下的 Jar 和 Class 文件。
如果需要加載指定位置的類(lèi)文件并實(shí)現(xiàn)一些自定義邏輯,就需要自定義類(lèi)加載器。
Chaya:如何實(shí)現(xiàn)自定義類(lèi)加載器?
步驟:
- 繼承 java.lang.ClassLoader 類(lèi)。
- 重寫(xiě) findClass() 方法,通過(guò)字節(jié)流讀取 Class 文件并轉(zhuǎn)換為 Class 對(duì)象。
import java.io.*;
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
return defineClass(name, classData, 0, classData.length);
}
private byte[] loadClassData(String name) {
String fileName = name.replace('.', '/') + ".class";
try (InputStream is = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
int buffer;
while ((buffer = is.read()) != -1) {
baos.write(buffer);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
示例說(shuō)明
- findClass():從文件系統(tǒng)加載 Class 文件,并將其定義為 Class 對(duì)象。
- defineClass():將字節(jié)數(shù)組轉(zhuǎn)換為 JVM 可執(zhí)行的 Class 對(duì)象。
為了為保證類(lèi)加載器都正確實(shí)現(xiàn)雙親委派機(jī)制,在開(kāi)發(fā)自己的類(lèi)加載器時(shí),只需要重寫(xiě) findClass() 方法即可。
當(dāng)然,如果不想使用雙親委派機(jī)制時(shí),就需要重寫(xiě) loadClass() 方法。
打破雙親委派模型
有時(shí)為了實(shí)現(xiàn)特殊功能,我們需要打破雙親委派模型,例如:
- 熱部署框架:Tomcat、Spring Boot 使用自定義類(lèi)加載器加載和卸載 Web 應(yīng)用。
- SPI(Service Provider Interface)機(jī)制:JDBC 驅(qū)動(dòng)等需要通過(guò) 線程上下文類(lèi)加載器 來(lái)加載用戶(hù)實(shí)現(xiàn)的接口。