Java 程式啟動時,**並非一次性載入所有類別**,而是**按需動態載入**。類加載器負責將 `.class` 位元組碼讀入 JVM 並生成 java.lang.Class 物件。理解類加載機制、雙親委派模型、類身份判定,是排查 **ClassNotFoundException、NoClassDefFoundError、類衝突、熱部署、模組化** 等疑難雜症的鑰匙。
Class 物件(堆上)。static 欄位分配記憶體,**賦予預設值**(0、null、false)。static 區塊、static 欄位賦值語句、** 方法**。關鍵點:
final static編譯期常數(基本型別/String)**準備階段即賦予最終值**,初始化階段**不再執行賦值**,直接讀取常量池。**存取此類常數不會觸發類初始化**。
JVM 內建三大加載器(啟動時建立):
| 加載器 | 類別名 | 載入範圍 | 父加載器 | 實現語言 |
|---|---|---|---|---|
| 啟動類加載器 | 無 (C++ 實現) | JAVA_HOME/lib/rt.jar 等核心庫 | 無 (頂層) | C++ |
| 擴展類加載器 | sun.misc.Launcher$ExtClassLoader | JAVA_HOME/lib/ext/*.jar 或 java.ext.dirs | 啟動類加載器 | Java |
| 應用/系統類加載器 | sun.misc.Launcher$AppClassLoader | classpath (-cp 指定路徑) | 擴展類加載器 | Java |
java.lang.*)由啟動加載器統一載入,**防止惡意竄改**、**保證類別身份唯一**。// 同一 JVM 中,類別唯一性 = 全限定名 + 類加載器實例
// 即:同名類別若由不同加載器載入,視為**不同類別**
// 相互不可轉型、static 欄位獨立、singleton 各自一份
package com.stopTalking.crazy;
public class TestClassLoader {
public static void main(String[] args) {
// 1. 當前執行緒上下文類加載器(通常 = AppClassLoader)
ClassLoader ctx = Thread.currentThread().getContextClassLoader();
System.out.println("ContextClassLoader: " + ctx);
// 2. 系統類 System 的類加載器(核心類 → Bootstrap → null)
ClassLoader sysLoader = System.class.getClassLoader();
System.out.println("System.class.getClassLoader(): " + sysLoader); // null
// 3. 本類別的類加載器(AppClassLoader)
ClassLoader myLoader = TestClassLoader.class.getClassLoader();
System.out.println("TestClassLoader.class.getClassLoader(): " + myLoader);
// 4. 父加載器鏈
ClassLoader parent = myLoader.getParent(); // ExtClassLoader
System.out.println("Parent: " + parent);
ClassLoader grandParent = parent.getParent(); // Bootstrap → null
System.out.println("GrandParent: " + grandParent);
}
}
ContextClassLoader: sun.misc.Launcher$AppClassLoader@6b97fd
System.class.getClassLoader(): null ← Bootstrap (C++)
TestClassLoader.class.getClassLoader(): sun.misc.Launcher$AppClassLoader@6b97fd
Parent: sun.misc.Launcher$ExtClassLoader@1c78e57
GrandParent: null ← Bootstrap
public class MyClassLoader extends ClassLoader {
private final String basePath; // 類別檔案根目錄
public MyClassLoader(String basePath) {
this.basePath = basePath;
}
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
// 1. 讀取 .class 位元組碼
byte[] classData = loadClassData(name);
if (classData == null) throw new ClassNotFoundException(name);
// 2. 定義類別
return defineClass(name, classData, 0, classData.length);
}
private byte[] loadClassData(String name) {
// 將包名轉路徑:com.example.Foo → com/example/Foo.class
String path = basePath + name.replace('.', '/') + ".class";
try (InputStream is = new FileInputStream(path)) {
return is.readAllBytes(); // Java 9+
} catch (IOException e) {
return null;
}
}
}
loadClass() 而非 findClass(),如 Tomcat WebappClassLoader 優先自載再委派父層。Class.forName("com.example.Foo"):**使用呼叫者的類加載器**載入並**初始化**類別。Class.forName("com.example.Foo", false, customLoader):指定加載器,**不初始化**(僅加載+連接)。customLoader.loadClass("com.example.Foo"):僅加載+連接,**不初始化**。clazz.newInstance() / constructor.newInstance():實例化,**需類別已初始化**。| 異常/現象 | 成因 | 排查方向 |
|---|---|---|
ClassNotFoundException | 類別檔案不在任何加載器搜尋路徑 | 檢查 classpath、模組依賴、打包是否遺漏 |
NoClassDefFoundError | 類曾載入成功,初始化失敗(static 區塊拋異常),後續再用 | 查看初始化時的根因異常(Cause) |
ClassCastException | 同名類別由不同加載器載入,實例不可轉型 | 檢查是否重複打包、模組邊界、雙親委派被破壞 |
LinkageError | 類別定義不一致(版本衝突、重複載入) | 依賴樹分析、強制統一版本 |
| 記憶體洩漏 (PermGen/Metaspace OOM) | 類加載器無法被 GC 回收(靜態引用、執行緒本地變數、JDBC 驅動註冊) | 排查 ClassLoader 引用鏈、使用弱引用、手動 deregister |
ClassLoader::getPlatformClassLoader() 取得平台加載器。findClass**,遵循委派模型;**熱部署/隔離**核心手段。-verbose:class)。核心洞見:Java 的動態性本質是**「類別在運行時由類加載器按需載入」**。掌握類加載器,就是掌握了 Java 運行時類別宇宙的「邊界與秩序」。