我們來深入解析 Java 類加載機制。這是理解 Java 應用如何運行、如何實現插件化、以及解決一些依賴沖突問題的關鍵。
一、核心概念:類加載過程
一個類型(包括類和接口)從被加載到虛擬機內存開始,到卸載出內存為止,它的整個生命周期會經歷加載(Loading)、連接(Linking)、初始化(Initialization)、使用(Using)和卸載(Unloading)?五個階段。其中連接階段又分為驗證(Verification)、準備(Preparation)、解析(Resolution)?三步。
加載: 查找并加載類的二進制字節流(如?
.class
?文件),并為其在方法區創建一個?java.lang.Class
?對象作為訪問入口。驗證: 確保被加載的類的正確性和安全性,如文件格式、元數據、字節碼、符號引用驗證。
準備: 為類變量(static 變量)?分配內存并設置默認初始值(零值)。例如?
public static int value = 123;
?在此階段后?value
?為?0
。解析: 將常量池內的符號引用替換為直接引用的過程。
初始化: 執行類構造器?
<clinit>()
?方法的過程,該方法由編譯器自動收集類中所有類變量的賦值動作和靜態語句塊(static{}塊)?中的語句合并產生。此時?value
?才會被賦值為?123
。
重要原則:《Java虛擬機規范》嚴格規定了有且只有以下六種情況必須立即對類進行“初始化”:
遇到?
new
,?getstatic
,?putstatic
,?invokestatic
?這四條字節碼指令時。使用?
java.lang.reflect
?包的方法對類進行反射調用時。當初始化一個類時,發現其父類還未初始化,需先觸發其父類的初始化。
虛擬機啟動時,用戶指定的主類(包含?
main()
?方法的那個類)。當使用 JDK 7 新加入的動態語言支持時...
當一個接口中定義了 JDK 8 新加入的默認方法(
default
方法)時...
二、雙親委派模型 (Parents Delegation Model)
Java 虛擬機如何決定由哪個類加載器來加載一個類?答案就是雙親委派模型。
1. 類加載器層次結構
JVM 提供了三層類加載器:
啟動類加載器 (Bootstrap ClassLoader):
C++ 實現,是 JVM 自身的一部分。
負責加載?
<JAVA_HOME>/lib
?目錄下的核心類庫(如?rt.jar
,?charsets.jar
)或被?-Xbootclasspath
?參數指定的路徑中的類。是所有類加載器的父加載器。
擴展類加載器 (Extension ClassLoader):
Java 實現,繼承自?
ClassLoader
。負責加載?
<JAVA_HOME>/lib/ext
?目錄下,或由?java.ext.dirs
?系統變量指定的路徑中的所有類庫。
應用程序類加載器 (Application ClassLoader):
Java 實現,繼承自?
ClassLoader
。也叫系統類加載器 (System ClassLoader)。
負責加載用戶類路徑 (ClassPath)?上的所有類庫。
是程序中默認的類加載器。
ClassLoader.getSystemClassLoader()
?返回的就是它。
除了這三個,用戶還可以自定義類加載器。
2. 雙親委派的工作流程
當一個類加載器收到了類加載請求時,它不會自己去嘗試加載,而是會把這個請求委托給父類加載器去完成。每一層的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中。只有當父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。
流程可以概括為:自底向上檢查類是否已加載 -> 自頂向下嘗試加載類。
圖表
3. 雙親委派的優點
避免類的重復加載: 確保一個類在 JVM 中全局唯一。無論哪一個類加載器要加載這個類,最終都是委派給最頂層的啟動類加載器去加載,從而保證了類的唯一性。
保證核心 API 的安全: 防止用戶自定義一個核心類(例如?
java.lang.String
)來替換掉 Java 核心庫中的類,從而確保了 Java 核心庫的安全性和穩定性。例如,即使用戶寫了一個?java.lang.Object
?類并放在 ClassPath 中,最終也只會由 Bootstrap ClassLoader 去加載核心庫中的?Object
?類,用戶的類不會被加載。
三、打破雙親委派模型
雙親委派模型不是一個強制性的約束,而是 Java 設計者推薦給開發者的一種類加載器實現方式。在某些特定場景下,需要打破這個模型。
1. 歷史原因:自身的缺陷
雙親委派模型很好地解決了各個類加載器協作時基礎類型的一致性問題(越基礎的類由越上層的加載器進行加載)。但如果基礎類型要調用回用戶的代碼,雙親委派模型就無法解決了。一個典型的例子就是?JNDI(Java Naming and Directory Interface),它的代碼由啟動類加載器加載,但需要調用由獨立廠商實現并部署在應用程序 ClassPath 下的 JNDI 服務提供者接口(SPI)的代碼。啟動類加載器不可能“認識”這些用戶代碼。
2. 解決方案:線程上下文類加載器 (Thread Context ClassLoader)
為了解決這個問題,Java 引入了線程上下文類加載器。這個類加載器可以通過?java.lang.Thread.setContextClassLoader()
?方法進行設置,如果創建線程時未設置,它將會從父線程中繼承一個;如果在應用程序的全局范圍內都沒有設置過,那默認就是應用程序類加載器。
SPI(Service Provider Interface)機制(如 JDBC)就利用了這個方案:
java.sql.DriverManager
(由 Bootstrap ClassLoader 加載)在初始化時,會通過?ServiceLoader
?去加載?java.sql.Driver
?接口的實現類。ServiceLoader
?的?load
?方法會使用線程上下文類加載器(默認是 AppClassLoader)去加載 META-INF/services 目錄下的配置文件中的實現類。這樣,位于 ClassPath 下的第三方數據庫驅動包(如?
mysql-connector-java.jar
)中的實現類(如?com.mysql.cj.jdbc.Driver
)就能被成功加載并注冊了。
這個過程可以看作是:父類加載器(Bootstrap)請求子類加載器(AppClassLoader)來完成類加載動作,這種行為實際上打破了雙親委派模型的層次結構,是一種逆向的委托。
3. 其他打破雙親委派的場景
熱部署、熱替換: 例如 OSGi、JSP 應用服務器(如 Tomcat)。每個模塊(Bundle)都有自己的類加載器,當需要更換一個 Bundle 時,就把 Bundle 連同類加載器一起換掉,從而實現代碼的熱替換。
實現應用隔離: 例如 Tomcat 為每個 Web 應用創建一個獨立的?
WebappClassLoader
,優先加載?/WEB-INF/
?下的類,應用之間互不干擾,這同樣打破了雙親委派(它沒有先委托給父加載器,而是自己先嘗試加載)。
四、自定義類加載器
1. 為什么要自定義?
從非標準來源加載類(如網絡、加密的字節流)。
實現類的隔離和熱部署。
修改類的加載方式(如打破雙親委派)。
2. 如何實現?
通常不直接重寫?loadClass()
?方法(因為該方法實現了雙親委派邏輯),而是重寫?findClass(String name)
?方法。
步驟:
繼承?
java.lang.ClassLoader
。重寫?
findClass
?方法:在這個方法中,根據指定的類名(如?
com.example.MyClass
)去查找類的字節碼(byte[]
)。找到后,調用父類的?
defineClass(byte[] b, int off, int len)
?方法,將字節數組轉換為?Class
?對象。
(可選)如果要打破雙親委派,可以重寫?
loadClass
?方法,實現自己的加載邏輯。
示例代碼框架:
java
public class MyClassLoader extends ClassLoader {private String classPath; // 自定義的類查找路徑@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {// 1. 根據 name 和 classPath,找到 .class 文件,讀取為 byte[]byte[] classData = loadClassData(name);if (classData == null) {throw new ClassNotFoundException();} else {// 2. 調用 defineClass,將字節碼定義為 Class 對象return defineClass(name, classData, 0, classData.length);}}private byte[] loadClassData(String name) {// 實現從自定義路徑(文件、網絡等)加載類的字節碼的邏輯// 將包名中的 '.' 替換為路徑分隔符 '/'String path = name.replace('.', '/').concat(".class");path = classPath + "/" + path;// ... 讀取文件并返回 byte[]return data;}
}
總結
概念 | 核心要點 |
---|---|
雙親委派模型 | 保證核心類安全、避免重復加載。工作流程:子加載器委托父加載器加載,父加載器無法完成時子加載器才自己加載。 |
打破雙親委派 | SPI 等場景需要父加載器請求子加載器完成加載。通過線程上下文類加載器實現,是 Java 生態擴展性的重要基礎。 |
自定義類加載器 | 重寫?findClass ?方法,從特定來源獲取字節碼后調用?defineClass 。常用于加載非標準來源的類、實現隔離和熱部署。 |
理解類加載機制,尤其是雙親委派和打破它的場景,對于解決日常開發中的 ClassNotFoundException、NoClassDefFoundError、依賴沖突以及構建模塊化系統都至關重要。