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