前言
Java 程式從啟動到執行,經歷了「JVM DLL 載入 → JNI 環境建立 → 類載入器層級委派 → 字節碼驗證/準備/解析/初始化」的完整鏈路。理解這兩大核心流程,不僅有助於排查 UnsatisfiedLinkError、NoClassDefFoundError 等疑難雜症,更是掌握熱部署、模組化、GraalVM 原生編譯等進階技術的基石。本文將從原始碼視角,拆解 HotSpot JVM 在 Windows 平台的啟動載入與類載入全過程。
一、JVM DLL 載入流程(Windows 平台)
1.1 啟動入口:java.exe 的四大步驟
- 建立 JVM 載入環境與配置:解析命令列參數、環境變數。
- 載入 jvm.dll:透過
LoadLibrary 載入動態連結庫。
- 初始化 JVM 並綁定 JNIEnv:呼叫
JNI_CreateJavaVM 取得 JNIEnv* 介面指標。
- 載入主類並啟動:透過
JNIEnv->FindClass 載入含 main 方法的類,呼叫 CallStaticVoidMethod 執行。
1.2 關鍵路徑解析:JRE_HOME 怎麼找到的?
java.exe 透過 GetApplicationHome(java_md.c)取得自身路徑,依序嘗試:
<java.exe目錄>/../jre/bin/java.dll 存在 → <java.exe目錄>/.. 為 JRE_HOME
<java.exe目錄>/../jre/bin/java.dll 存在 → <java.exe目錄>/../jre 為 JRE_HOME
- 讀取註冊表
HKLM\Software\JavaSoft\Java Runtime Environment\<版本>\JavaHome
可用 System.getProperty("java.home") 驗證結果。
1.3 JVM.cfg:多 JVM 版本的選擇策略
JRE_HOME/lib/<arch>/jvm.cfg 定義支援的 JVM 類型:
-client KNOWN
-server KNOWN
-hotspot ALIASED_TO -client
-classic WARN
-native ERROR
-green ERROR
KNOWN:該 JVM 存在
ALIASED_TO:別名指向
WARN:缺失時降級替換
ERROR:缺失拋異常
啟動時可透過 -client、-server、-XXaltJVM=<path> 指定;未指定則取 cfg 第一個 KNOWN 項。設定 _JAVA_LAUNCHER_DEBUG=1 可在主控台列印解析細節。
1.4 JNI 綁定與主類啟動
LoadLibrary 載入 jvm.dll,取得 JNI_CreateJavaVM、JNI_GetDefaultJavaVMInitArgs 函數指標。
- 呼叫
JNI_CreateJavaVM 獲得 JNIEnv 實例。
- 若為
-jar:解析 META-INF/MANIFEST.MF 的 Main-Class;否則直接用命令列類名。
FindClass 載入主類 → GetStaticMethodID 找 main → CallStaticVoidMethod 執行。
二、JVM 類載入體系:雙親委派與模組化演進
2.1 三階段七步驟標準流程
- 載入:尋找並導入二進制資料 → 生成
Class 物件
- 連結(可分步、解析可選)
- 驗證:檢查字節碼合法性
- 準備:靜態欄位分配記憶體、賦預設值
- 解析:符號引用 → 直接引用
- 初始化:執行
<clinit>(靜態變數賦值、靜態區塊)
2.2 ClassLoader 核心方法與自定義實現
| 方法 | 用途 |
loadClass(name, resolve) | 入口點,雙親委派邏輯核心 |
defineClass(bytes) | 字節碼 → Class 物件(解析、驗證) |
findSystemClass(name) | 從本地檔案系統載入(Bootstrap 預設機制) |
resolveClass(Class) | 觸發解析階段 |
findLoadedClass(name) | 檢查是否已載入,避免重複 |
自定義 ClassLoader 範本
public class MultiClassLoader extends ClassLoader {
private Map<String, Class<?>> cache = new HashMap<>();
@Override
public synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 1. 快取檢查
Class<?> clazz = cache.get(name);
if (clazz != null) return clazz;
// 2. 委派父載入器(系統載入器)
try {
clazz = super.findSystemClass(name);
return clazz;
} catch (ClassNotFoundException ignored) {}
// 3. 自定義載入邏輯(網路、加密、動態生成等)
byte[] bytes = loadClassBytes(name); // 子類實現
if (bytes == null) throw new ClassNotFoundException();
clazz = defineClass(null, bytes, 0, bytes.length);
if (resolve) resolveClass(clazz);
cache.put(name, clazz);
return clazz;
}
protected abstract byte[] loadClassBytes(String name);
}
2.3 雙親委派模型與載入器層級
Bootstrap ClassLoader (C++ 實現,null 代表)
↑
Extension ClassLoader (Java 實現,載入 jre/lib/ext)
↑
Application ClassLoader (載入 CLASSPATH,用戶自定義載入器預設父)
↑
自定義 ClassLoader (Applet、Tomcat WebappClassLoader、OSGi 模組載入器...)
- 委派規則:子載入器收到請求 → 先委派父載入器 → 父無法載入 → 子自行載入。
- 隔離性:同名類由不同載入器載入視為不同類型(
ClassCastException 根因)。
- 可見性:子載入器可見父載入器類,父不可見子載入器類。
2.4 實戰案例:Tomcat 打破雙親委派實現 Web 應用隔離
Tomcat WebappClassLoader 先嘗試自行載入 Webapp 目錄類(WEB-INF/classes、WEB-INF/lib),失敗再委派父載入器。確保各 Webapp 可使用不同版本同名庫(如 Log4j 1.x vs 2.x),互不干擾。
三、模組化時代(Java 9+)的類載入變革
- 模組系統:
module-info.java 宣告依賴、導出封裝,載入器按模組圖解析,強封裝內部 API。
- 層級變化:Bootstrap → Platform → Application,Extension 機制移除。
- 非標準 API 遷移:
--add-exports、--add-opens 替代舊有破壞封裝手段。
四、總結與排查清單
| 現象 | 可能原因 | 排查方向 |
UnsatisfiedLinkError | jvm.dll 路徑錯誤、架構不匹配(x86/x64)、原生庫缺失 | 檢查 java.library.path、JRE_HOME 解析、_JAVA_LAUNCHER_DEBUG |
NoClassDefFoundError | 類初始化失敗、載入器委派鏈斷裂、模組未導出 | -verbose:class 追蹤載入順序、檢查 module-info |
ClassCastException(同名類) | 雙親委派被破壞、多載入器重複載入 | 確認載入器層級、ClassLoader.getClassLoader() 比對 |
掌握 JVM DLL 載入與類載入雙主線,能讓你從「會用 Java」進階到「懂 Java 運作本質」。建議收藏排查清單,下次遇到載入異常不再無從下手。
- 上一篇文章: Hibernate 緩存與延遲加載機制深度解析:一級/二級緩存、查詢緩存、實體/集合/屬性三級延遲
- 下一篇文章: Java 字串處理核心:String、StringBuffer、StringBuilder 深度對比與選型指南
Copyright ©
程式碼之家 All Rights Reserved