1. JDK, JRE和JVM的關系
- JDK = JRE + Java開發工具
- JRE = JVM + Java核心類庫
JDK供Java程序開發人員開發軟件,JRE供客戶使用,只需要JVM運行環境即可。
JVM運行的是class字節碼,不僅能運行Java代碼,還能運行其他語言,只要語言能編譯成字節碼文件即可,比如Kotlin。
2. JVM的主要組成部分
- 類加載器
- 加載Loading
- 鏈接Linking
- 初始化Initialization
- 運行時數據區
- 執行引擎
- 本地接口
3. 你能解釋一下JVM類加載器的作用嗎
1. 加載 (Loading)
- 目的:將類的?
.class
?文件的二進制數據讀入內存,并在方法區中創建一個?java.lang.Class
?對象來表示這個類。 - 主要工作:
- 通過類的全限定名獲取其二進制字節流:這可以通過多種方式實現,比如從本地?
.class
?文件、JAR 包、網絡、動態生成(如?Proxy
?類)、數據庫等。 - 將字節流代表的靜態存儲結構轉化為方法區的運行時數據結構:JVM 解析字節碼,并在方法區(Method Area)或元空間(Metaspace,Java 8+)中創建該類的數據結構。
- 在內存中創建一個?
java.lang.Class
?對象:這個對象是?java.lang.Class
?的實例,它作為程序訪問該類各種數據的入口。這個對象通常存儲在堆(Heap)中。
- 通過類的全限定名獲取其二進制字節流:這可以通過多種方式實現,比如從本地?
- 關鍵點:
- 這個階段主要由類加載器(ClassLoader)?完成。
- 類加載器遵循雙親委派模型(Parent Delegation Model),即先委托父類加載器嘗試加載,只有當父類加載器無法完成時,子加載器才會嘗試自己加載。
2. 鏈接 (Linking)
鏈接階段確保加載的類是正確且符合 JVM 規范的,并為其分配內存。它分為三個子階段:
(1) 驗證 (Verification)
- 目的:確保?
.class
?文件的字節流包含的信息符合當前 JVM 的要求,不會危害 JVM 的安全。 - 主要檢查:
- 文件格式驗證:檢查字節流是否符合?
.class
?文件格式規范(如魔數?0xCAFEBABE
、版本號等)。 - 元數據驗證:檢查類的元數據信息是否有矛盾(如是否繼承了?
final
?類、是否實現了不存在的接口等)。 - 字節碼驗證:這是最復雜和關鍵的一步。通過數據流和控制流分析,確定字節碼指令不會做出危害 JVM 安全的操作(如類型轉換錯誤、非法跳轉、訪問不存在的字段等)。
- 符號引用驗證:確保解析動作能正常執行(如檢查符號引用中描述的類、字段、方法是否存在)。
- 文件格式驗證:檢查字節流是否符合?
- 重要性:這是 JVM 防止惡意代碼攻擊的重要屏障。雖然驗證很耗時,但可以保證運行時的安全性。
(2) 準備 (Preparation)
- 目的:為類的靜態變量(
static
?fields)分配內存,并設置這些變量的初始值。 - 關鍵點:
- 分配內存:在方法區(或元空間)為?
static
?變量分配內存。 - 設置初始值:這里的“初始值”通常是零值(zero value),而不是你在代碼中賦的值。
int
?類型的?static
?變量初始值為?0
。boolean
?類型的?static
?變量初始值為?false
。- 引用類型(
Object
)的?static
?變量初始值為?null
。
final static
?常量:如果?static
?變量同時被?final
?修飾,并且是基本類型或?String
?字面量,那么它的值(編譯期常量)會在這個階段直接賦值,而不是零值。例如:public static final int MAX = 100;
?的值?100
?會在準備階段就設置好。
- 分配內存:在方法區(或元空間)為?
(3) 解析 (Resolution)
- 目的:將常量池內的符號引用(Symbolic References)替換為直接引用(Direct References)。
- 概念解釋:
- 符號引用:以一組符號來描述所引用的目標。它可以是任何形式的字面量,只要能無歧義地定位到目標即可。例如,常量池中用?
類名.方法名.描述符
?來表示一個方法。 - 直接引用:可以直接指向目標的指針、相對偏移量或一個能間接定位到目標的句柄。直接引用是與內存布局相關的。
- 符號引用:以一組符號來描述所引用的目標。它可以是任何形式的字面量,只要能無歧義地定位到目標即可。例如,常量池中用?
- 解析的內容:
- 類或接口解析:將符號引用解析為具體的類或接口的?
Class
?對象。 - 字段解析:將符號引用解析為字段在類中的內存偏移量。
- 方法解析:將符號引用解析為方法在方法表中的索引或直接指針。
- 接口方法解析:類似方法解析。
- 類或接口解析:將符號引用解析為具體的類或接口的?
- 時機:解析動作不一定在鏈接階段一次性完成,它可能在初始化之后才進行(稱為“延遲解析”或“惰性解析”)。只有當真正需要使用某個符號引用時,才會觸發解析。
3. 初始化 (Initialization)
- 目的:執行類的初始化代碼,為類的靜態變量賦予程序中指定的值,并執行?
static
?代碼塊。 - 主要工作:
- 執行?
<clinit>()
?方法。<clinit>
?是由編譯器自動收集類中所有?static
?變量的賦值語句和?static
?代碼塊中的語句合并產生的類構造器方法。 - 按照代碼中出現的順序執行這些初始化語句。
- 執行?
- 關鍵點:
- 這是類加載過程的最后一步。
<clinit>()
?方法是線程安全的:JVM 會保證一個類的?<clinit>()
?方法在多線程環境下只被執行一次。其他線程會阻塞,直到第一個線程完成初始化。- 觸發時機:這是主動使用一個類的時刻。以下操作會觸發類的初始化:
- 創建類的實例(
new
?關鍵字)。 - 訪問類的靜態變量(
public static
?除外,final static
?編譯期常量也不會觸發)。 - 調用類的靜態方法。
- 使用反射(
Class.forName()
)。 - 初始化一個類的子類(會先觸發父類的初始化)。
- 虛擬機啟動時,包含?
main()
?方法的主類。 MethodHandle
?和?VarHandle
?的某些操作。
- 創建類的實例(
- 被動引用不會觸發:訪問?
final static
?編譯期常量、通過子類引用父類的?static
?變量(只會觸發父類初始化,不會觸發子類)、數組定義(new MyClass[10]
?不會觸發?MyClass
?的初始化)等屬于被動引用,不會觸發初始化。
4. 使用 (Using)
- 目的:類初始化完成后,就可以被程序正常使用了。
- 工作:程序通過?
new
?創建對象、調用靜態方法、訪問實例方法等。
5. 卸載 (Unloading)?
- 目的:當類不再被任何地方引用,滿足垃圾回收條件時,JVM 可以卸載該類,回收其占用的內存(主要是方法區/元空間和?
Class
?對象本身)。 - 條件:非常嚴格。需要該類的?
ClassLoader
?被回收、該類的所有實例都已被回收、該類的?Class
?對象沒有被任何地方引用。
總結流程圖
加載 (Loading)↓
鏈接 (Linking)├── 驗證 (Verification)├── 準備 (Preparation) <-- static 變量賦零值 (或 final static 常量值)└── 解析 (Resolution) <-- 符號引用 -> 直接引用↓
初始化 (Initialization) <-- 執行 <clinit>(), static 變量賦程序值, 執行 static 塊↓
使用 (Using)↓
卸載 (Unloading) (可選)
4. 你知道JVM的類加載器有哪些?雙親委派機制是什么?
一、JVM 的類加載器 (Class Loaders)
JVM 在啟動時會創建一系列的類加載器,它們形成了一個層次結構。主要的類加載器有三種(從頂層到底層):
1. 啟動類加載器 (Bootstrap ClassLoader)
- 角色:最頂層的類加載器,是 JVM?自身的一部分,通常由 C/C++ 實現。
- 負責加載:
JAVA_HOME/lib
?目錄下的核心類庫(如?rt.jar
,?tools.jar
,?resources.jar
?等)。- 或者被?
-Xbootclasspath
?參數指定的路徑中的類庫。
- 特點:
- 用 C/C++ 編寫,不是 Java 類,因此在 Java 代碼中無法直接引用它(
getClassLoader()
?返回?null
)。 - 負責加載最基礎、最核心的 Java 類(如?
java.lang.*
,?java.util.*
,?java.io.*
?等)。
- 用 C/C++ 編寫,不是 Java 類,因此在 Java 代碼中無法直接引用它(
2. 擴展類加載器 (Extension ClassLoader)
- 角色:
Bootstrap ClassLoader
?的子加載器,由 Java 實現。 - 負責加載:
JAVA_HOME/lib/ext
?目錄下的類庫。- 或者被?
java.ext.dirs
?系統變量所指定的路徑中的所有類庫。
- 特點:
- 允許開發者將具有通用功能的 JAR 包放在這個目錄下,自動被加載,無需在?
-classpath
?中指定。 - 在 Java 9 的模塊化系統(JPMS)之后,其重要性有所下降。
- 允許開發者將具有通用功能的 JAR 包放在這個目錄下,自動被加載,無需在?
3. 應用程序類加載器 (Application ClassLoader) / 系統類加載器 (System ClassLoader)
- 角色:
Extension ClassLoader
?的子加載器,也是 Java 實現。 - 負責加載:
- 用戶類路徑(ClassPath)上所指定的類庫。
- 即我們通常通過?
-classpath
?或?-cp
?參數指定的?.jar
?文件或?.class
?文件目錄。
- 特點:
- 這是默認的類加載器,我們編寫的 Java 類和第三方依賴庫(如 Maven/Gradle 依賴)通常由它加載。
- 可以通過?
ClassLoader.getSystemClassLoader()
?獲取它的實例。
(可選) 自定義類加載器 (Custom ClassLoader)
- 角色:開發者可以繼承?
java.lang.ClassLoader
?類來創建自己的類加載器。 - 目的:
- 從非標準來源加載類(如網絡、數據庫、加密的 JAR 包)。
- 實現類的隔離(如 Tomcat 的 Web 應用隔離、OSGi 模塊化)。
- 實現熱部署(Hot Swap)。
- 常用場景:Web 服務器(Tomcat, Jetty)、應用服務器(WebLogic, WebSphere)、插件化框架、熱更新系統。
二、雙親委派機制 (Parent Delegation Model)
雙親委派機制是 JVM 類加載器加載類時遵循的一種工作模式。它的核心思想是:當一個類加載器收到類加載請求時,它不會自己先去加載,而是把這個請求委派給它的父類加載器去完成,每一層的類加載器都是如此,因此所有的加載請求最終都會傳送到頂層的啟動類加載器。只有當父類加載器無法完成這個加載請求(即在它的搜索路徑下找不到所需的類)時,子加載器才會嘗試自己去加載。
工作流程
- 發起請求:假設應用程序類加載器(AppClassLoader)收到一個加載?
java.lang.String
?的請求。 - 向上委派:AppClassLoader 不會直接加載,而是將請求委派給它的父加載器——擴展類加載器(ExtClassLoader)。
- 繼續委派:ExtClassLoader 收到請求后,也不會直接加載,而是繼續委派給它的父加載器——啟動類加載器(Bootstrap ClassLoader)。
- 頂層嘗試加載:Bootstrap ClassLoader 嘗試在?
rt.jar
?等核心庫中查找?java.lang.String
,找到了,于是加載成功,返回?Class
?對象。 - 逐層返回:加載結果從 Bootstrap ClassLoader 逐層返回給 ExtClassLoader,再返回給 AppClassLoader,最終返回給發起請求的代碼。
如果父加載器找不到呢?
- 假設請求加載一個用戶自定義的類?
com.example.MyClass
。 - 請求最終傳到 Bootstrap ClassLoader,它在核心庫中找不到。
- Bootstrap ClassLoader 返回失敗。
- 請求返回到 ExtClassLoader,它在?
lib/ext
?目錄下也找不到。 - 請求返回到 AppClassLoader,它在 ClassPath 下找到了?
com.example.MyClass.class
,于是由它自己加載。
為什么需要雙親委派機制?
避免類的重復加載:
- 保證一個類在 JVM 中只有一個唯一的?
Class
?對象。 - 例如,無論哪個類加載器發起加載?
java.lang.Object
?的請求,最終都會由 Bootstrap ClassLoader 加載,確保所有地方使用的都是同一個?Object
?類。
- 保證一個類在 JVM 中只有一個唯一的?
保證核心類庫的安全性:
- 這是最關鍵的一點。它防止了惡意代碼通過自定義類加載器來替換核心 Java 類。
- 例如,你不能自己寫一個?
java.lang.String
?類放在 ClassPath 下,期望它被加載。因為當請求到達時,Bootstrap ClassLoader 會先加載它自己的、可信的?String
?類,你的惡意類永遠沒有機會被加載。 - 這確保了 Java 核心 API 的穩定性和安全性。
如何打破雙親委派?
雖然雙親委派是默認和推薦的模式,但在某些特殊場景下需要打破它:
基礎類型回調用戶代碼:
- 典型例子:
JNDI
?(Java Naming and Directory Interface)。 JNDI
?的核心類由 Bootstrap ClassLoader 加載,但它需要回調由應用程序提供的服務實現(SPI - Service Provider Interface)。- Bootstrap ClassLoader 無法加載應用類路徑下的類。
- 解決方案:通過線程上下文類加載器(
Thread.currentThread().getContextClassLoader()
)。這個加載器通常被設置為 AppClassLoader。JNDI
?核心代碼可以通過它來加載用戶實現的 SPI 類,從而“逆向”委托。
- 典型例子:
實現熱部署/模塊化:
- 典型例子:Tomcat, OSGi。
- 需要隔離不同 Web 應用或模塊的類,避免相互影響和核心庫沖突。
- 解決方案:自定義類加載器,并重寫?
loadClass()
?方法,改變委派邏輯。例如,Tomcat 的 Web 應用類加載器會優先嘗試自己加載 Web 應用的類(/WEB-INF/classes
,?/WEB-INF/lib
),只有當自己找不到時,才委派給父加載器(打破了“先委派”的原則)。這實現了應用間的類隔離。
總結
- 類加載器:Bootstrap -> Extension -> Application -> Custom,形成層次結構。
- 雙親委派:加載請求優先向上委派給父加載器,父加載器無法完成時,子加載器才嘗試自己加載。
- 優點:保證類的唯一性、核心類庫安全。
- 打破場景:SPI(如 JNDI)、熱部署/模塊化(如 Tomcat, OSGi),通常通過線程上下文類加載器或重寫?
loadClass()
?實現。