Java 的類別載入機制是 JVM 執行 Java 程式碼的第一步。理解類別載入過程,對於解決 ClassNotFoundException、理解框架原理、實現熱部署等場景至關重要。
類別載入的三個階段#
Java 類別的載入過程分為三個主要階段:載入(Loading)、連結(Linking)、初始化(Initialization)。
flowchart LR
subgraph Loading["載入 Loading"]
L1[讀取位元組碼]
end
subgraph Linking["連結 Linking"]
direction LR
V[驗證] --> P[準備] --> R[解析]
end
subgraph Init["初始化 Initialization"]
I1[執行 clinit]
end
Loading --> Linking --> Init1. 載入(Loading)#
載入階段負責查找並讀取類別的位元組碼。
// JVM 在載入階段會執行以下操作:
// 1. 透過類別的全限定名獲取位元組流
// 2. 將位元組流轉換為方法區的執行時資料結構
// 3. 在堆積區建立 java.lang.Class 物件作為存取入口類別的位元組碼可以來自多種來源:本地 .class 檔案、JAR/WAR 套件、網路、動態生成(如動態代理)等。
2. 連結(Linking)#
連結階段分為三個子階段:
驗證(Verification)#
驗證位元組碼的正確性與安全性,確保不會危害 JVM。
驗證內容:
├── 檔案格式驗證:魔數 0xCAFEBABE、版本號等
├── 中繼資料驗證:語義分析,如是否有父類別
├── 位元組碼驗證:資料流與控制流分析
└── 符號參照驗證:確保解析能正常執行準備(Preparation)#
為類別變數(static 變數)分配記憶體並設定初始值。
public class Example {
// 準備階段:value = 0(int 的預設值)
// 初始化階段:value = 123
public static int value = 123;
// 準備階段:CONSTANT = 100(final 常數直接賦值)
public static final int CONSTANT = 100;
}準備階段只處理類別變數(static),不處理實體變數。實體變數會在物件實體化時隨物件一起分配在堆積區。
各資料型別的初始值
| 資料型別 | 初始值 |
|---|---|
byte | (byte) 0 |
short | (short) 0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0d |
char | '\u0000' |
boolean | false |
| 參照型別 | null |
解析(Resolution)#
將常數池中的符號參照替換為直接參照。
// 編譯期:使用符號參照
// "java/lang/String" (字串形式的類別名)
// 解析後:使用直接參照
// 指向方法區中 String 類別資料的指標3. 初始化(Initialization)#
執行類別的初始化程式碼,包括靜態變數賦值和靜態區塊。
public class InitExample {
static int a = 10; // 靜態變數賦值
static {
System.out.println("靜態區塊執行");
a = 20;
}
// <clinit> 方法會依序執行:
// 1. a = 10
// 2. 執行靜態區塊,a = 20
}JVM 會保證
<clinit>()方法在多執行緒環境下被正確地加鎖同步,因此靜態內部類別單例模式是執行緒安全的。
類別初始化的觸發時機#
JVM 規範定義了以下六種情況會觸發類別初始化:
// 1. new、getstatic、putstatic、invokestatic 指令
MyClass obj = new MyClass(); // new 指令
int value = MyClass.staticValue; // getstatic 指令
MyClass.staticValue = 100; // putstatic 指令
MyClass.staticMethod(); // invokestatic 指令
// 2. 反射呼叫
Class.forName("com.example.MyClass");
// 3. 初始化子類別時,父類別尚未初始化
class Parent {}
class Child extends Parent {} // 初始化 Child 會先初始化 Parent
// 4. JVM 啟動時的主類別
public static void main(String[] args) // 包含 main 方法的類別
// 5. MethodHandle 解析結果為 REF_getStatic 等
// 6. 介面定義了 default 方法,實作類別初始化時不會觸發初始化的情況
// 1. 透過子類別存取父類別的靜態欄位,只會初始化父類別
class Parent {
static int value = 100;
}
class Child extends Parent {}
System.out.println(Child.value); // 不會初始化 Child
// 2. 陣列定義
Parent[] array = new Parent[10]; // 不會初始化 Parent
// 3. 常數在編譯期進入常數池,不會觸發類別初始化
public static final String CONSTANT = "hello";
System.out.println(MyClass.CONSTANT); // 不會初始化 MyClass雙親委派模型#
類別載入器層級#
graph TD
B["Bootstrap ClassLoader<br/>啟動類別載入器<br/><i>JAVA_HOME/lib</i>"]
E["Extension ClassLoader<br/>擴展類別載入器<br/><i>JAVA_HOME/lib/ext</i>"]
A["Application ClassLoader<br/>應用程式類別載入器<br/><i>classpath</i>"]
C["Custom ClassLoader<br/>自訂類別載入器"]
B --> E --> A --> C
style B fill:#e1f5fe
style E fill:#fff3e0
style A fill:#e8f5e9
style C fill:#fce4ec從 Java 9 開始,Extension ClassLoader 被 Platform ClassLoader(平台類別載入器)取代,但雙親委派的核心機制保持不變。
雙親委派機制#
// ClassLoader.loadClass() 的簡化邏輯
protected Class<?> loadClass(String name, boolean resolve) {
// 1. 檢查類別是否已載入
Class<?> c = findLoadedClass(name);
if (c == null) {
// 2. 委派給父類別載入器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 到達 Bootstrap ClassLoader
c = findBootstrapClassOrNull(name);
}
// 3. 父類別載入器無法載入,自己嘗試載入
if (c == null) {
c = findClass(name);
}
}
return c;
}flowchart TB
REQ["請求載入<br/>com.example.MyClass"] --> APP
subgraph Delegate["委派流程"]
direction TB
APP["Application ClassLoader"] -->|委派| EXT["Extension ClassLoader"]
EXT -->|委派| BOOT["Bootstrap ClassLoader"]
end
subgraph Return["返回流程"]
direction TB
BOOT2["Bootstrap ClassLoader"] -->|找不到| EXT2["Extension ClassLoader"]
EXT2 -->|找不到| APP2["Application ClassLoader"]
APP2 -->|自己載入| FOUND["載入成功"]
end
BOOT -.-> BOOT2
style Delegate fill:#e3f2fd
style Return fill:#fff9c4
style FOUND fill:#c8e6c9雙親委派的優點#
雙親委派模型確保了 Java 核心類別庫的安全性與一致性。例如,無論哪個類別載入器要載入
java.lang.Object,最終都會委派給 Bootstrap ClassLoader,保證所有環境下 Object 類別都是同一個。
// 嘗試自訂一個 java.lang.String 類別?
package java.lang;
public class String {
// 這個類別永遠不會被載入
// 因為會優先委派給 Bootstrap ClassLoader
// Bootstrap ClassLoader 會載入 JDK 的 String
}打破雙親委派#
某些場景需要打破雙親委派機制:
1. SPI 機制#
// JDBC 驅動載入是經典案例
// DriverManager 在 rt.jar 中,由 Bootstrap ClassLoader 載入
// 但具體的驅動(如 MySQL Driver)在 classpath 中
// 使用執行緒上下文類別載入器(Thread Context ClassLoader)解決
Thread.currentThread().setContextClassLoader(classLoader);
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);2. 熱部署#
// 實現熱部署需要自訂類別載入器
public class HotSwapClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 特定類別不委派,直接自己載入
if (name.startsWith("com.myapp.hotswap")) {
return findClass(name);
}
return super.loadClass(name);
}
@Override
protected Class<?> findClass(String name) {
// 從指定位置讀取最新的 class 檔案
byte[] classData = loadClassData(name);
return defineClass(name, classData, 0, classData.length);
}
}3. OSGi 模組化#
OSGi 的網狀類別載入結構:
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Bundle A │ ←→ │ Bundle B │ ←→ │ Bundle C │
└─────────┘ └─────────┘ └─────────┘
↑ ↑ ↑
└───────────────┴───────────────┘
│
┌────────▼────────┐
│ System Bundle │
└─────────────────┘自訂類別載入器#
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 將類別名轉換為檔案路徑
String fileName = classPath + File.separator
+ name.replace(".", File.separator) + ".class";
// 讀取位元組碼
byte[] classData = Files.readAllBytes(Paths.get(fileName));
// 定義類別
return defineClass(name, classData, 0, classData.length);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
}
}
// 使用自訂類別載入器
MyClassLoader loader = new MyClassLoader("/path/to/classes");
Class<?> clazz = loader.loadClass("com.example.MyClass");
Object instance = clazz.getDeclaredConstructor().newInstance();AppCDS 應用程式類別資料共享#
從 Java 10 開始,AppCDS(Application Class-Data Sharing)可以顯著加速應用程式啟動時間,特別適合微服務和容器化部署。
# 步驟 1:建立要共享的類別清單
java -Xshare:off -XX:+UseAppCDS \
-XX:DumpLoadedClassList=classes.lst \
-cp myapp.jar com.example.Main
# 步驟 2:建立共享檔案
java -Xshare:dump -XX:+UseAppCDS \
-XX:SharedClassListFile=classes.lst \
-XX:SharedArchiveFile=app-cds.jsa \
-cp myapp.jar
# 步驟 3:使用共享檔案啟動
java -Xshare:on -XX:+UseAppCDS \
-XX:SharedArchiveFile=app-cds.jsa \
-cp myapp.jar com.example.MainAppCDS 效果評估
在典型的 Spring Boot 應用中,AppCDS 可以:
- 減少 10-30% 的啟動時間
- 降低多個 JVM 實體的記憶體佔用(共享唯讀資料)
- 特別適合 Kubernetes 中多副本部署的場景
測試指令:
# 不使用 AppCDS
time java -jar myapp.jar &
# 等待啟動完成後記錄時間
# 使用 AppCDS
time java -Xshare:on -XX:SharedArchiveFile=app-cds.jsa -jar myapp.jar &
# 比較啟動時間常見問題與排查#
ClassNotFoundException vs NoClassDefFoundError#
// ClassNotFoundException:執行時找不到類別
// 通常是 Class.forName() 或 ClassLoader.loadClass() 時發生
try {
Class.forName("com.example.NotExist");
} catch (ClassNotFoundException e) {
// 類別路徑中沒有這個類別
}
// NoClassDefFoundError:編譯時存在,執行時不存在
// 或者類別初始化失敗
public class Example {
// 如果 DependencyClass 在執行時不存在
// 會拋出 NoClassDefFoundError
private DependencyClass dep = new DependencyClass();
}類別載入相關的 JVM 參數#
# 查看類別載入過程
-verbose:class
-XX:+TraceClassLoading
-XX:+TraceClassUnloading
# 指定類別路徑
-classpath / -cp
-Xbootclasspath
# 類別資料共享
-Xshare:on/off/auto
-XX:SharedArchiveFile=<file>總結#
類別載入機制的核心要點:
- 三階段:載入 → 連結(驗證、準備、解析)→ 初始化
- 雙親委派:保證核心類別安全,避免重複載入
- 觸發時機:new、靜態存取、反射、子類別初始化等
- 打破委派:SPI、熱部署、模組化等場景需要
- 實用技巧:AppCDS 可加速啟動,自訂載入器實現特殊需求