摘要:
JVM是Java程序運行的核心環境,負責解釋執行字節碼并管理內存。其核心功能包括類加載與驗證、字節碼執行優化、內存管理與垃圾回收(GC)、跨平臺支持及安全性保障。JVM架構包含程序計數器、虛擬機棧、本地方法棧、堆和方法區等內存區域,其中堆分為新生代(Eden、Survivor區)和老年代,采用不同GC策略(MinorGC和FullGC)。類加載遵循雙親委派機制,但Tomcat等容器會打破該機制以實現應用隔離。垃圾回收通過可達性分析算法判斷對象是否可回收,主要觸發條件包括內存不足、手動調用System.gc()或達到閾值。JVM的自動內存管理機制有效減少了內存泄漏風險。
一、JVM 概述
(一)JVM 定義與核心地位
定義:
JVM是一個虛擬的計算機系統?,通過解釋和執行Java字節碼(.class文件)來運行Java程序。它由字節碼指令集、寄存器、棧、垃圾回收堆和方法區等核心組件構成,本質是遵循Java SE規范的抽象計算環境。JVM的執行引擎基于棧結構,能夠將編譯后的字節碼轉換為具體硬件平臺的機器指令。
核心地位:
跨平臺能力的基石
JVM通過“一次編譯,到處運行?”的特性實現平臺無關性。Java源代碼編譯為與平臺無關的字節碼后,JVM屏蔽底層硬件和操作系統的差異,確保程序在不同環境中一致運行。Java生態的運行基礎
作為Java程序的唯一運行環境?,JVM不僅管理內存、執行代碼,還通過垃圾回收機制優化資源利用。所有Java技術(如框架、庫)均依賴JVM提供的運行時支持。技術演進的核心驅動力
JVM的設計目標是構建可擴展、高性能的執行環境。其規范由Java社區進程(JCP)持續更新,推動著Java語言及工具鏈的發展。
(二)JVM 主要功能
一、類加載與驗證
- 動態加載與鏈接
JVM通過類加載器(ClassLoader)加載編譯后的.class
文件,并按需動態鏈接類(如驗證字節碼合法性、解析符號引用)。例如,驗證階段會檢查字節碼是否符合規范,防止惡意代碼注入 - 層次化加載機制
采用雙親委派模型?,確保核心類庫(如java.lang.*
)優先由Bootstrap ClassLoader(啟動類加載器)加載,避免重復加載和安全風險
二、字節碼執行與優化
- 解釋與編譯混合執行
- 解釋器?
逐條執行字節碼,確保跨平臺兼容性。
- 即時編譯器(JIT)?
將熱點代碼(頻繁執行的方法)編譯為本地機器指令,提升性能。例如,HotSpot JVM的C1/C2編譯器分別優化低延遲和高吞吐場景。
- 解釋器?
- 執行引擎控制流
通過程序計數器(PC寄存器)?記錄當前線程執行位置,支持分支、循環等復雜邏輯。
三、內存管理與垃圾回收
- 運行時數據區劃分
- 堆(Heap)?存儲對象實例和數組,是垃圾回收的主要區域
- 方法區(Metaspace)?存放類元數據、常量池和靜態變量(JDK 8后移至本地內存)
- 棧(Stack)?
每個線程私有的棧幀存儲局部變量、操作數棧、動態鏈接和方法出口等。
- 自動內存回收
通過垃圾回收器(GC)?自動管理堆內存,標記并清理不可達對象。例如,G1 GC通過分區回收減少停頓時間
四、跨平臺與安全性
- “一次編譯,到處運行”
字節碼與平臺無關,JVM屏蔽底層硬件/操作系統差異,實現跨平臺兼容 - 安全機制
類加載驗證、字節碼校驗及運行時權限檢查(如SecurityManager
)共同保障代碼安全性
五、本地方法接口(JNI?Java Native Interface)
通過本地庫接口?調用C/C++等本地代碼,擴展JVM功能(如高性能計算或硬件交互)。例如,Java的System.currentTimeMillis()
依賴本地方法實現。
六、性能監控與調優
JVM提供JMX(Java Management Extensions)?等工具接口,支持實時監控內存、線程狀態及GC行為,助力性能優化。
二、JVM 架構解析
1.JVM的內存模型
根據 JDK 8 規范,JVM 運行時內存共分為虛擬機棧、堆、元空間、程序計數器、本地方法棧五個部分。還有一部分內存叫直接內存,屬于操作系統的本地內存,也是可以直接操作的。
JVM的內存結構主要分為以下幾個部分:
程序計數器:可以看作是當前線程所執行的字節碼的行號指示器,用于存儲當前線程正在執行的 Java 方法的 JVM?指令地址。如果線程執行的是 Native 方法,計數器值為 null。是唯一一個在 Java 虛擬機規范中沒有規定任何 OutOfMemoryError 情況的區域,生命周期與線程相同。
Java 虛擬機棧:每個線程都有自己獨立的 Java 虛擬機棧,生命周期與線程相同。每個方法在執行時都會創建一個棧幀,用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。可能會拋出 StackOverflowError 和 OutOfMemoryError 異常。
本地方法棧:與 Java 虛擬機棧類似,主要為虛擬機使用到的 Native 方法服務,在 HotSpot 虛擬機中和 Java 虛擬機棧合二為一。本地方法執行時也會創建棧幀,同樣可能出現 StackOverflowError 和 OutOfMemoryError 兩種錯誤。
Java 堆:是 JVM 中最大的一塊內存區域,被所有線程共享,在虛擬機啟動時創建,用于存放對象實例和數組。從內存回收角度,堆被劃分為新生代和老年代,新生代又分為 Eden 區和兩個 Survivor 區(From Survivor 和 To Survivor)。如果在堆中沒有內存完成實例分配,并且堆也無法擴展時會拋出 OutOfMemoryError 異常。
方法區(元空間):在 JDK 1.8 及以后的版本中,方法區被元空間取代,使用本地內存。用于存儲已被虛擬機加載的類信息、常量、靜態變量等數據。雖然方法區被描述為堆的邏輯部分,但有 “非堆” 的別名。方法區可以選擇不實現垃圾收集,內存不足時會拋出 OutOfMemoryError 異常。
運行時常量池:是方法區的一部分,用于存放編譯期生成的各種字面量和符號引用,具有動態性,運行時也可將新的常量放入池中。當無法申請到足夠內存時,會拋出 OutOfMemoryError 異常。
直接內存:不屬于 JVM 運行時數據區的一部分,通過 NIO 類引入,是一種堆外內存,可以顯著提高 I/O 性能。直接內存的使用受到本機總內存的限制,若分配不當,可能導致 OutOfMemoryError 異常。
這里講講堆:
Java堆(Heap)是Java虛擬機(JVM)中內存管理的一個重要區域,主要用于存放對象實例和數組。隨著JVM的發展和不同垃圾收集器的實現,堆的具體劃分可能會有所不同,但通常可以分為以下幾個部分:
新生代(Young Generation):新生代分為Eden Space和Survivor Space。在Eden Space中, 大多數新創建的對象首先存放在這里。Eden區相對較小,當Eden區滿時,會觸發一次Minor GC(新生代垃圾回收)。在Survivor Spaces中,通常分為兩個相等大小的區域,稱為S0(Survivor 0)和S1(Survivor 1)。在每次Minor GC后,存活下來的對象會被移動到其中一個Survivor空間,以繼續它們的生命周期。這兩個區域輪流充當對象的中轉站,幫助區分短暫存活的對象和長期存活的對象。
補充下Minor GC
Minor GC(次要垃圾回收)是針對新生代(Young Generation)的垃圾回收過程,主要目標是快速清理 Eden 區和 Survivor 區中的短生命周期對象。以下是其具體過程:
1.?標記存活對象
從?GC Roots(如線程棧、靜態變量等)出發,標記所有可達對象。
新生代的對象通常生命周期短,大部分對象會在此階段被判定為垃圾。
2.?復制存活對象(Copying)
Eden → Survivor
將 Eden 區和當前使用的 Survivor(From 區)中的存活對象復制到另一個 Survivor(To 區)。
年齡增長
每經歷一次 Minor GC,存活對象的年齡(Age)加 1。
晉升老年代
當對象年齡超過閾值(默認 15,可通過?
-XX:MaxTenuringThreshold
?調整)或 Survivor 區空間不足時,對象會被晉升到老年代。
3.?清理垃圾
直接清空 Eden 和 From 區(無需復雜清理,因存活對象已被復制走)。
交換 Survivor 區的角色:原來的 To 區變為下一次 GC 的 From 區。
4.?觸發條件
Eden 區空間不足時觸發(通常由新對象分配請求觸發)。
可通過 JVM 參數調整新生代大小(如?
-Xmn
)。
特點
STW(Stop-The-World)
暫停所有應用線程,但時間極短(毫秒級)。
高效
采用復制算法,避免內存碎片。
高頻
發生頻率高于 Full GC。
老年代(Old Generation/Tenured Generation):存放過一次或多次Minor GC仍存活的對象會被移動到老年代。老年代中的對象生命周期較長,因此Major GC(也稱為Full GC,涉及老年代的垃圾回收)發生的頻率相對較低,但其執行時間通常比Minor GC長。老年代的空間通常比新生代大,以存儲更多的長期存活對象。
元空間(Metaspace):從Java 8開始,永久代(Permanent Generation)被元空間取代,用于存儲類的元數據信息,如類的結構信息(如字段、方法信息等)。jdk1.8后,元空間并不在Java堆中,而是使用本地內存,這解決了永久代容易出現的內存溢出問題。
大對象區(Large Object Space / Humongous Objects):在某些JVM實現中(如G1垃圾收集器),為大對象分配了專門的區域,稱為大對象區或Humongous Objects區域。大對象是指需要大量連續內存空間的對象,如大數組。這類對象直接分配在老年代,以避免因頻繁的年輕代晉升而導致的內存碎片化問題。
這里再補充一下Full GC(完全垃圾回收)
Full GC(完全垃圾回收)是針對?整個堆內存(包括新生代、老年代)以及?方法區(元空間)?的垃圾回收過程,其觸發條件更嚴格且耗時更長。以下是具體過程、觸發條件和優化要點:
Full GC 的具體過程
標記階段(Marking)
從所有?GC Roots(線程棧、靜態變量、JNI引用等)出發,遞歸標記堆中所有存活對象。
若使用?并發標記算法(如CMS、G1),會盡量減少STW時間。
清理階段(Cleaning)
Serial/Parallel Old
標記-整理(Mark-Compact),避免內存碎片。
CMS
并發標記-清除(Mark-Sweep),會產生碎片。
G1
分區(Region)回收,優先清理垃圾比例高的區域。
新生代
采用復制算法(Minor GC),存活對象晉升到老年代或Survivor區。
老年代
根據垃圾回收器不同,算法各異:
方法區(元空間)
卸載無用的類、常量等。
壓縮階段(可選)
部分回收器(如Parallel Old)會壓縮老年代,減少碎片,提升內存分配效率。
Full GC 的觸發條件
老年代空間不足
Minor GC后存活對象需晉升到老年代,但老年代剩余空間不足(可能因?
-XX:SurvivorRatio
?或?-XX:MaxTenuringThreshold
?設置不合理)。Promotion Failed
新生代對象晉升失敗時觸發。
方法區(元空間)不足
類加載過多或元空間配置過小(
-XX:MaxMetaspaceSize
)。
顯式調用
通過?
System.gc()
?或?Runtime.getRuntime().gc()
?觸發(建議禁用:-XX:+DisableExplicitGC
)。
分配擔保失敗
Minor GC前,若老年代剩余空間小于新生代對象總大小(或歷次晉升平均大小),會先嘗試Full GC。
CMS/G1的并發模式失敗
CMS并發清理期間老年代空間耗盡,或G1無法在預期時間內完成回收。
Full GC 的性能影響
STW(Stop-The-World)
暫停所有應用線程,耗時可能達?秒級(與堆大小、對象數量正相關)。
CPU密集型
標記和壓縮階段會占用大量CPU資源。
碎片問題
CMS等基于標記-清除的回收器可能導致老年代碎片,最終觸發壓縮式Full GC。
為什么大對象一般都放在老年代?
大對象通常會直接分配到老年代。
新生代主要用于存放生命周期較短的對象,并且其內存空間相對較小。如果將大對象分配到新生代,可能會很快導致新生代空間不足,從而頻繁觸發 Minor GC。而每次 Minor GC 都需要進行對象的復制和移動操作,這會帶來一定的性能開銷。將大對象直接分配到老年代,可以減少新生代的內存壓力,降低 Minor GC 的頻率。
大對象通常需要連續的內存空間,如果在新生代中頻繁分配和回收大對象,容易產生內存碎片,導致后續分配大對象時可能因為內存不連續而失敗。老年代的空間相對較大,更適合存儲大對象,有助于減少內存碎片的產生。
再看看棧:
JVM 中的?棧(Stack)?是線程私有的內存區域,用于存儲方法調用時的?棧幀(Stack Frame),每個方法從調用到執行完成對應一個棧幀的入棧和出棧過程。棧的大小決定了方法調用的深度(可通過?-Xss
?調整,如?-Xss1M
)。以下是核心要點:
1. 棧的結構
線程私有
每個線程創建時分配一個獨立的棧,生命周期與線程相同。
棧幀(Stack Frame)
每個方法調用會壓入一個棧幀,包含以下內容:
局部變量表(Local Variables)
存儲方法參數和局部變量,以?變量槽(Slot)?為單位(32位占1 Slot,64位如?
long
/double
?占2 Slot)。
示例:void foo(int a, long b)
?的局部變量表索引0是?a
,索引1-2是?b
。操作數棧(Operand Stack)
用于執行字節碼指令的臨時數據存儲(如加減乘除操作時的中間結果)。
示例:iadd
?指令會從操作數棧彈出兩個?int
?相加后壓回棧頂。動態鏈接(Dynamic Linking)
指向運行時常量池中該方法的符號引用,用于支持多態(如虛方法調用)。
方法返回地址(Return Address)
方法正常退出或異常退出時,返回調用者的位置(PC寄存器值)。
2. 棧的運作示例
public?class?Main?{
public?static?void?main(String[] args)?{
int?x =?1;
int?y =?add(x,?2);?// 調用add方法
}
static?int?add(int?a,?int?b)?{
return?a + b;
}
}
執行流程:
main
?方法棧幀入棧,局部變量表存儲?
args
、x
、y
。調用?
add
?方法時,add
?棧幀入棧:操作數棧壓入?
a=1
?和?b=2
。執行?
iadd
?后,棧頂結果為?3
。
add
?棧幀出棧,返回值賦給?
main
?棧幀的?y
。
3. 棧的異常
- StackOverflowError
當棧深度超過?
-Xss
?限制時拋出(如無限遞歸調用)。
void?infinite() {?infinite(); }?// 遞歸調用導致棧溢出
- OutOfMemoryError
線程創建過多導致棧總內存耗盡(需減少線程數或增大?
-Xss
)。
4. 棧 vs 堆
特性 | 棧 | 堆 |
---|---|---|
內存分配 | 自動分配/釋放(方法結束彈出) | 由GC管理 |
存儲內容 | 局部變量、方法調用鏈 | 對象實例、數組 |
線程共享 | 線程私有 | 線程共享 |
訪問速度 | 快(直接操作內存) | 慢(需指針尋址) |
更具體一些:
用途:棧主要用于存儲局部變量、方法調用的參數、方法返回地址以及一些臨時數據。每當一個方法被調用,一個棧幀(stack frame)就會在棧中創建,用于存儲該方法的信息,當方法執行完畢,棧幀也會被移除。堆用于存儲對象的實例(包括類的實例和數組)。當你使用new關鍵字創建一個對象時,對象的實例就會在堆上分配空間。
生命周期:棧中的數據具有確定的生命周期,當一個方法調用結束時,其對應的棧幀就會被銷毀,棧中存儲的局部變量也會隨之消失。堆中的對象生命周期不確定,對象會在垃圾回收機制(Garbage Collection, GC)檢測到對象不再被引用時才被回收。
存取速度:棧的存取速度通常比堆快,因為棧遵循先進后出(LIFO, Last In First Out)的原則,操作簡單快速,直接操作內存。堆的存取速度相對較慢,因為對象在堆上的分配和回收需要更多的時間,而且垃圾回收機制的運行也會影響性能。
存儲空間:棧的空間相對較小,且固定,由操作系統管理。當棧溢出時,通常是因為遞歸過深或局部變量過大。堆的空間較大,動態擴展,由JVM管理。堆溢出通常是由于創建了太多的大對象或未能及時回收不再使用的對象。
可見性:棧中的數據對線程是私有的,每個線程有自己的棧空間。堆中的數據對線程是共享的,所有線程都可以訪問堆上的對象。
常見的疑惑:棧中存的到底是指針還是對象?
在JVM內存模型中,棧(Stack)主要用于管理線程的局部變量和方法調用的上下文,而堆(Heap)則是用于存儲所有類的實例和數組。
當我們在棧中討論“存儲”時,實際上指的是存儲基本類型的數據(如int, double等)和對象的引用,而不是對象本身。
這里的關鍵點是,棧中存儲的不是對象,而是對象的引用。也就是說,當你在方法中聲明一個對象,比如MyObject obj = new MyObject();,這里的obj實際上是一個存儲在棧上的引用,指向堆中實際的對象實例。這個引用是一個固定大小的數據(例如在64位系統上是8字節),它指向堆中分配給對象的內存區域。
三、類初始化和類加載
在 JVM 中,類加載(Class Loading)?和?類初始化(Class Initialization)?是兩個不同的階段,但經常被混淆。下面詳細解析它們的區別、觸發條件及執行過程。
1. 類加載(Class Loading)
類加載?是指將?.class
?字節碼文件加載到 JVM 內存,并生成?Class<?>
?對象的過程。它由?類加載器(ClassLoader)?完成,分為?加載、連接(驗證、準備、解析)、初始化?三個階段。
類從被加載到虛擬機內存開始,到卸載出內存為止,它的整個生命周期包括以下 7 個階段:
加載:通過類的全限定名(包名 + 類名),獲取到該類的.class文件的二進制字節流,將二進制字節流所代表的靜態存儲結構,轉化為方法區運行時的數據結構,在內存中生成一個代表該類的Java.lang.Class對象,作為方法區這個類的各種數據的訪問入口。(簡單來說就是在程序運行時,通過類的全限定名找到對應的.class文件,將其讀入內存并將其中的靜態存儲結構轉化為方法區中的數據結構,最終生成一個代表該類的Class對象,方便程序對該類進行各種操作。)
連接:驗證、準備、解析 3 個階段統稱為連接。
驗證:確保class文件中的字節流包含的信息,符合當前虛擬機的要求,保證這個被加載的class類的正確性,不會危害到虛擬機的安全。驗證階段大致會完成以下四個階段的檢驗動作:文件格式校驗、元數據驗證、字節碼驗證、符號引用驗證(確保?
.class
?文件符合 JVM 規范(如魔數檢查、字節碼驗證)。)準備:為類中的靜態字段分配內存(在方法區),并設置默認的初始值(如?
int=0
,boolean=false
,引用類型?null
)。(但是被final修飾的static字段不會設置,因為final在編譯的時候就分配了。如?static final int x = 123
在此階段直接賦值。)解析:解析階段是虛擬機將常量池的「符號引用」直接替換為「直接引用」的過程。符號引用是以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用的時候可以無歧義地定位到目標即可。直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄,直接引用是和虛擬機實現的內存布局相關的。如果有了直接引用, 那引用的目標必定已經存在在內存中了。(將符號引用(如?
java/lang/Object
)轉換為直接引用(內存地址)。)
初始化:初始化是整個類加載過程的最后一個階段,初始化階段簡單來說就是執行類的構造器方法(
<clinit>()
?方法),要注意的是這里的構造器方法(<clinit>()
?方法)并不是開發者寫的,而是編譯器自動生成的。
使用:使用類或者創建對象。
卸載:如果有下面的情況,類就會被卸載:1. 該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例。2. 加載該類的ClassLoader已經被回收。 3. 類對應的Java.lang.Class對象沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法。
對類初始化的一些需要清楚的點:
類初始化?是類加載的最后一步,執行?<clinit>()
?方法(由編譯器自動生成),完成?靜態變量賦值?和?靜態代碼塊執行。
(1) 觸發條件(嚴格規定)
JVM 規范規定?以下 6 種情況會觸發初始化:
new
、getstatic
、putstatic
、invokestatic
?指令(如?
new
?實例、訪問靜態變量/方法)。- 反射調用
(如?
Class.forName("com.example.Test")
)。 - 子類初始化時,父類未初始化則先初始化父類。
- 主類(包含?
main()
?的類)在啟動時初始化。 java.lang.invoke.MethodHandle
?動態調用時(涉及?
invokedynamic
?指令)。- 接口的默認方法(JDK 8+)被實現類初始化時。
(2) 不會觸發初始化的場景
- 訪問?
static final
?常量(已在準備階段賦值)。
- 通過數組定義引用類
(如?
Test[] arr = new Test[10]
)。 - 通過類名訪問?
Class
?對象(如?
Test.class
)。 - 調用?
ClassLoader.loadClass()
(僅加載,不初始化)。
(3)?<clinit>()
?方法的特點
- 由編譯器自動生成
合并所有靜態變量賦值和靜態代碼塊。
- 線程安全
(JVM 加鎖保證只執行一次)。
- 父類?
<clinit>()
?先執行(父類靜態代碼塊優先于子類)。
示例:
class?Parent?{
? ??static?int?x =?10;?// (1)
? ??static?{
? ? ? ??int?x =?20;?// (2)
? ? ? ? System.out.println("Parent static block executed, local x = "?+ x);
? ? }
}
class?Child?extends?Parent?{
? ??static?int?y =?20;?// (3)
? ??static?{
? ? ? ??int?y =?40;?// (4)
? ? ? ? System.out.println("Child static block executed, local y = "?+ y);
? ? }
}
public?class?Main?{
? ??public?static?void?main(String[] args)?{
? ? ? ? System.out.println("Parent.x = "?+ Parent.x);
? ? ? ? System.out.println("Child.y = "?+ Child.y);
? ? }
}
執行順序:
(1) → (2) → (3) → (4)
(父類優先)。
類加載器有哪些?
啟動類加載器(Bootstrap Class Loader):這是最頂層的類加載器,負責加載Java的核心庫(如位于jre/lib/rt.jar中的類),它是用C++編寫的,是JVM的一部分。啟動類加載器無法被Java程序直接引用。
擴展類加載器(Extension Class Loader):它是Java語言實現的,繼承自ClassLoader類,負責加載Java擴展目錄(jre/lib/ext或由系統變量Java.ext.dirs指定的目錄)下的jar包和類庫。擴展類加載器由啟動類加載器加載,并且父加載器就是啟動類加載器。
系統類加載器(System Class Loader)/ 應用程序類加載器(Application Class Loader):這也是Java語言實現的,負責加載用戶類路徑(ClassPath)上的指定類庫,是我們平時編寫Java程序時默認使用的類加載器。系統類加載器的父加載器是擴展類加載器。它可以通過ClassLoader.getSystemClassLoader()方法獲取到。
自定義類加載器(Custom Class Loader):開發者可以根據需求定制類的加載方式,比如從網絡加載class文件、數據庫、甚至是加密的文件中加載類等。自定義類加載器可以用來擴展Java應用程序的靈活性和安全性,是Java動態性的一個重要體現。
這些類加載器之間的關系形成了雙親委派模型,其核心思想是當一個類加載器收到類加載的請求時,首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中。
只有當父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。(后面會細講)
總結一下就是:
啟動類加載器(Bootstrap):
- C++寫的,JVM親兒子
只管加載最核心的庫(比如
rt.jar
),Java代碼里調不到它。
- C++寫的,JVM親兒子
擴展類加載器(Extension):
- Java寫的,干雜活的
專加載
jre/lib/ext
目錄下的擴展包,爹是啟動類加載器。
- Java寫的,干雜活的
系統類/應用程序類加載器(Application):
- Java寫的,打工人
默認加載你寫的代碼(ClassPath路徑),爹是擴展類加載器,能直接通過
ClassLoader.getSystemClassLoader()
叫來干活。
- Java寫的,打工人
自定義類加載器(Custom):
- 你自己寫的,想咋加載就咋加載
(比如從網絡、數據庫撈class文件),靈活度拉滿,但得繼承
ClassLoader
類。
- 你自己寫的,想咋加載就咋加載
雙親委派機制:
- “拼爹”模式
兒子收到加載請求,先甩鍋給爹,爹搞不定兒子才自己上,最終鍋會甩到啟動類加載器。
- 好處
防止重復加載,保證核心庫的安全(比如你寫個
java.lang.String
也白搭,啟動類加載器早加載好了)。
一句話總結:從Bootstrap到Custom,一級級甩鍋,核心庫優先,自定義兜底。
雙親委派機制
先來談談雙親委派模型,簡單說就是當類加載器(Class-Loader)試圖加載某個類型的時候,除非父加載器找不到相應類型,否則盡量將這個任務代理給當前加載器的父加載器去做。使用委派模型的目的是避免重復加載 Java 類型。
雙親委派的工作流程
當需要加載一個類時,流程如下:
- 委托父加載器?當前類加載器首先將請求委托給父加載器,遞歸至頂層(Bootstrap ClassLoader)
。
- 父加載器嘗試加載?父加載器在其負責的路徑中查找并加載類。若成功,則直接返回
。
- 子加載器自行加載?若父加載器無法加載(如類不存在于父加載器的路徑),子加載器才會嘗試自己加載
。
例如,加載java.util.ArrayList
時,Application ClassLoader會依次委托Extension和Bootstrap ClassLoader,最終由Bootstrap加載。
🌰 示例場景:加載?java.lang.String
- 應用程序代碼
中寫了?
String s = new String();
,觸發?java.lang.String
?類的加載。 - AppClassLoader
(應用類加載器)收到請求,先委托給父加載器?ExtClassLoader。
- ExtClassLoader
?繼續向上委托給?BootstrapClassLoader(頂級加載器)。
- BootstrapClassLoader
?在?
JRE/lib/rt.jar
?中找到?java.lang.String
?并加載(成功)。如果找不到,會向下拋給?ExtClassLoader,再拋給?AppClassLoader。
🔍 關鍵流程(以?MyClass.class
?為例)
// 假設用戶自定義類 MyClass
public?class?MyClass?{
? ??public?void?print()?{
? ? ? ? System.out.println("Hello");
? ? }
}
- 用戶調用
new MyClass()
觸發加載。
- AppClassLoader?收到請求,委派鏈:
AppClassLoader → ExtClassLoader → BootstrapClassLoader
- BootstrapClassLoader
?和?ExtClassLoader?均無法加載(非核心類或擴展類),最終由?AppClassLoader?從?
classpath
?加載。
雙親委派模型的作用
保證類的唯一性:通過委托機制,確保了所有加載請求都會傳遞到啟動類加載器,避免了不同類加載器重復加載相同類的情況,保證了Java核心類庫的統一性,也防止了用戶自定義類覆蓋核心類庫的可能。
保證安全性:由于Java核心庫被啟動類加載器加載,而啟動類加載器只加載信任的類路徑中的類,這樣可以防止不可信的類假冒核心類,增強了系統的安全性。例如,惡意代碼無法自定義一個Java.lang.System類并加載到JVM中,因為這個請求會被委托給啟動類加載器,而啟動類加載器只會加載標準的Java庫中的類。
支持隔離和層次劃分:雙親委派模型支持不同層次的類加載器服務于不同的類加載需求,如應用程序類加載器加載用戶代碼,擴展類加載器加載擴展框架,啟動類加載器加載核心庫。這種層次化的劃分有助于實現沙箱安全機制,保證了各個層級類加載器的職責清晰,也便于維護和擴展。
簡化了加載流程:通過委派,大部分類能夠被正確的類加載器加載,減少了每個加載器需要處理的類的數量,簡化了類的加載過程,提高了加載效率。
沙箱安全機制
沙箱(sandbox)是一種用于隔離和保護計算機系統中的應用程序的安全機制。它通過將應用程序運行在獨立的虛擬環境中來防止惡意軟件對主機系統的攻擊。
具體來說,沙箱通常包括以下幾個組件:
沙箱環境:沙箱提供了一個隔離的應用程序運行環境,其中包含了操作系統、硬件設備和其他必要的資源。
安全策略:沙箱定義了一系列安全策略,例如限制應用程序可以訪問的文件和網絡資源等,以確保應用程序不會對主機系統造成損害。
監控和審計:沙箱會監控應用程序的行為,并記錄其所有的操作,以便后續分析和審計。
應用程序管理:沙箱提供了應用程序管理和部署的功能,使管理員能夠輕松地安裝、升級和卸載應用程序。
總之,沙箱安全機制可以幫助組織保護其敏感數據和系統免受惡意軟件的攻擊。
如何打破雙親委派機制?
先再看看另外一個更加清晰的委派機制流程圖
如何去打破雙親委派機制,這個問題很經典,面試如果問到JVM,這個問題大概率會被問到。
我們既然知道了類的加載方式默認是雙親委派,那么如果我們有一個類想要通過自定義的類加載器來加載這個類,而不是通過系統默認的類加載器,也就是不走雙親委派那一套,而是走自定義的類加載器。
首先我們得清楚,雙親委派的機制是ClassLoader類中的loadClass方法實現的,打破雙親委派,其實就是重寫這個方法,來用我們自己的方式來實現即可。
當然這里要注意一下,Object.class這是對象的頂級類,改變類的類加載器的時候要注意,如果全部改了,Object.class就找不到了,加載不了了
所以呢,這里重寫的時候,要注意分類解決,把你想要通過自定義類加載器加載的和想通過默認類加載器加載的分隔開。
如果不想打破雙親委派模型,就重寫ClassLoader類中的findClass()方法即可,無法被父類加載器加載的類最終會通過這個方法被加載。
而如果想打破雙親委派模型則需要重寫ClassLoader類的loadClass()方法(當然其中的坑也不會少)。典型的打破雙親委派模型的框架和中間件有tomcat與osgi
Tomcat是如何打破"雙親委派"機制的?
首先我們得想想,Tomcat為什么要打破"雙親委派"?
其目的其實很簡單,我們知道,web容器可能是需要部署多個應用程序的,上圖就是其中一個例子。但是假設不同的應用程序可能會同時依賴第三方類庫的不同版本。
而類加載機制是要確保唯一性的(上文原話:通過委托機制,確保了所有加載請求都會傳遞到啟動類加載器,避免了不同類加載器重復加載相同類的情況,保證了Java核心類庫的統一性,也防止了用戶自定義類覆蓋核心類庫的可能。),但是總不能要求同一個類庫在web容器中只有一份吧?所以Tomcat就需要保證每個應用程序的類庫都是相互隔離并獨立的,這也是它為什么打破雙親委派機制的主要目的。(Tomcat打破雙親委派機制的核心目的是解決多應用隔離問題。)
也就是說:
因為在一些情況下,應用程序需要加載一些不受控制的類。如果使用雙親委派機制,這些不受控制的類可能會被系統類庫中的同名類所覆蓋,導致程序出錯。
通過打破雙親委派機制,Tomcat可以自己負責加載應用程序所需的類,并且不會受到系統類庫的影響。這樣可以保證應用程序的穩定性和安全性。
用通俗例子解釋:
假設你有一個Tomcat服務器(相當于一個小區),里面部署了2個Web應用:
應用A 需要用老版本的
commons-lib.jar
(1.0版)應用B 需要用新版本的
commons-lib.jar
(2.0版)
如果遵循Java默認的雙親委派機制(小區共用同一個庫房):
類加載器會優先加載父容器的類
最終兩個應用會強制共用同一個版本的庫(比如先加載的1.0版)
導致應用B崩潰(版本不兼容)
Tomcat的解決方案(打破規則):
給每個應用配獨立的類加載器(每家有自己的小庫房)
應用A用自己的1.0版,應用B用自己的2.0版
互不干擾,實現隔離
本質就是:犧牲類加載的統一性,換取多版本庫的共存能力。
到這里,關于“Tomcat為什么要打破"雙親委派"?”的問題,相信你應該懂了。
Tomcat類加載概述
Tomcat的ClassLoader層級如下所示
CommonClassLoader(通用類加載器):是Tomcat中的一個類加載器,主要用于加載catalina.base/lib定義的目錄和jar以及{catalina.home}/lib定義的目錄和jar。它可以被Tomcat和所有的Web應用程序共同使用,實現了Web應用程序之間的類加載器相互隔離獨立的目的。
與之相對應的是WebAppClassLoader(Web應用的類加載器):它是Tomcat加載應用的核心類加載器,每個Web應用程序都有一個WebAppClassLoader,類庫僅僅可以被此Web應用程序使用,對Tomcat和其他Web程序都不可見。WebAppClassLoader打破了“雙親委派”機制,當收到類加載的請求時,它會先嘗試自己去加載,如果找不到則交給父加載器去加載,這樣做的目的是為了優先加載Web應用程序自己定義的類來實現Web應用程序相互隔離獨立的目標。
Tomcat類加載器初始化過程
我們可以在org.apache.catalina.startup.Bootstrap看到如下代碼:
???????
private?void?initClassLoaders()?{ ?
? ??try?{
? ? ? ? commonLoader = createClassLoader("common",?null);
? ? ? ??if( commonLoader ==?null?) {
? ? ? ? ? ? commonLoader=this.getClass().getClassLoader();
? ? ? ? } ?
? ? ? ??//初始化其它兩個類加載器 ?
? ? ? ? ? ? catalinaLoader = createClassLoader("server", commonLoader); ?
? ? ? ? ? ? sharedLoader = createClassLoader("shared", commonLoader); ?
? ? ? ? }?catch?(Throwable t) { ?
? ? ? ? ? ? log.error("Class loader creation threw exception", t); ?
? ? ? ? ? ? System.exit(1); ?
? ? ? ? } ?
? ? }
? ??private?void?initClassLoaders()?{
? ? ? ??try?{
? ? ? ? ? ??// 創建CommonClassLoader
? ? ? ? ? ? commonLoader = createClassLoader("common",?null);
? ? ? ? ? ??if( commonLoader ==?null?) {
? ? ? ? ? ? ? ??// no config file, default to this loader - we might be in a 'single' env.
? ? ? ? ? ? ? ? commonLoader=this.getClass().getClassLoader();
? ? ? ? ? ? }
? ? ? ? ? ??// 根據配置創建SharedClassLoader、CatalinaClassLoader
? ? ? ? ? ? catalinaLoader = createClassLoader("server", commonLoader);
? ? ? ? ? ? sharedLoader = createClassLoader("shared", commonLoader);
? ? ? ? }?catch?(Throwable t) {
? ? ? ? ? ? handleThrowable(t);
? ? ? ? ? ? log.error("Class loader creation threw exception", t);
? ? ? ? ? ? System.exit(1);
? ? ? ? }
? ? }
? ??private?ClassLoader?createClassLoader(String name, ClassLoader parent)
? ? ? ? throws Exception {
? ? ? ??// 讀取catalina.properties文件中的配置
? ? ? ? String?value?= CatalinaProperties.getProperty(name +?".loader");
? ? ? ??// 沒有對應的配置,不會創建此類加載器,而是返回傳入的父類加載器,也就是CommonClassLoader
? ? ? ??if?((value?==?null) || (value.equals("")))
? ? ? ? ? ??return?parent;
? ? ? ??value?= replace(value);
? ? ? ? List<Repository> repositories =?new?ArrayList<>();
? ? ? ? String[] repositoryPaths = getPaths(value);
? ? ? ??for?(String repository : repositoryPaths) {
? ? ? ? ? ??// Check for a JAR URL repository
? ? ? ? ? ??try?{
? ? ? ? ? ? ? ? @SuppressWarnings("unused")
? ? ? ? ? ? ? ? URL url =?new?URL(repository);
? ? ? ? ? ? ? ? repositories.add(
? ? ? ? ? ? ? ? ? ? ? ??new?Repository(repository, RepositoryType.URL));
? ? ? ? ? ? ? ??continue;
? ? ? ? ? ? }?catch?(MalformedURLException e) {
? ? ? ? ? ? ? ??// Ignore
? ? ? ? ? ? }
? ? ? ? ? ??// Local repository
? ? ? ? ? ??if?(repository.endsWith("*.jar")) {
? ? ? ? ? ? ? ? repository = repository.substring
? ? ? ? ? ? ? ? ? ? (0, repository.length() -?"*.jar".length());
? ? ? ? ? ? ? ? repositories.add(
? ? ? ? ? ? ? ? ? ? ? ??new?Repository(repository, RepositoryType.GLOB));
? ? ? ? ? ? }?else?if?(repository.endsWith(".jar")) {
? ? ? ? ? ? ? ? repositories.add(
? ? ? ? ? ? ? ? ? ? ? ??new?Repository(repository, RepositoryType.JAR));
? ? ? ? ? ? }?else?{
? ? ? ? ? ? ? ? repositories.add(
? ? ? ? ? ? ? ? ? ? ? ??new?Repository(repository, RepositoryType.DIR));
? ? ? ? ? ? }
? ? ? ? }
? ? ? ??return?ClassLoaderFactory.createClassLoader(repositories, parent);
? ? }
Tomcat是如何打破雙親委派機制的呢?
從上文中,我們不難看出,真正實現web應用程序之間的類加載器相互隔離獨立的是WebAppClassLoader類加載器。它為什么可以隔離每個web應用程序呢?原因就是它打破了"雙親委派"的機制,如果收到類加載的請求,它會先嘗試自己去加載,如果找不到再交給父加載器去加載,這么做的目的就是為了優先加載Web應用程序自己定義的類來實現web應用程序相互隔離獨立的。
WebappClassLoader底層原理
我們知道ClassLoader默認的loadClass方法是以雙親委派的模型進行加載類的,那么想要加載自定義資源打破"雙親委派"的機制,那么Tomcat就要必定要重寫findClass與loadClass方法,如下所示:
???????
? ??@Override
? ??public?Class<?> findClass(String name)?throws?ClassNotFoundException {
? ? ? ??if?(log.isDebugEnabled())
? ? ? ? ? ? log.debug(" ? ?findClass("?+ name +?")");
? ? ? ? checkStateForClassLoading(name);
? ? ? ??// (1) Permission to define this class when using a SecurityManager
? ? ? ??if?(securityManager !=?null) {
? ? ? ? ? ??int?i?=?name.lastIndexOf('.');
? ? ? ? ? ??if?(i >=?0) {
? ? ? ? ? ? ? ??try?{
? ? ? ? ? ? ? ? ? ??if?(log.isTraceEnabled())
? ? ? ? ? ? ? ? ? ? ? ? log.trace(" ? ? ?securityManager.checkPackageDefinition");
? ? ? ? ? ? ? ? ? ? securityManager.checkPackageDefinition(name.substring(0,i));
? ? ? ? ? ? ? ? }?catch?(Exception se) {
? ? ? ? ? ? ? ? ? ??if?(log.isTraceEnabled())
? ? ? ? ? ? ? ? ? ? ? ? log.trace(" ? ? ?-->Exception-->ClassNotFoundException", se);
? ? ? ? ? ? ? ? ? ??throw?new?ClassNotFoundException(name, se);
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? }
? ? ? ??// Ask our superclass to locate this class, if possible
? ? ? ??// (throws ClassNotFoundException if it is not found)
? ? ? ? Class<?> clazz =?null;
? ? ? ??try?{
? ? ? ? ? ??if?(log.isTraceEnabled())
? ? ? ? ? ? ? ? log.trace(" ? ? ?findClassInternal("?+ name +?")");
? ? ? ? ? ??try?{
? ? ? ? ? ? ? ??if?(securityManager !=?null) {
? ? ? ? ? ? ? ? ? ? PrivilegedAction<Class<?>> dp =
? ? ? ? ? ? ? ? ? ? ? ??new?PrivilegedFindClassByName(name);
? ? ? ? ? ? ? ? ? ? clazz = AccessController.doPrivileged(dp);
? ? ? ? ? ? ? ? }?else?{
? ? ? ? ? ? ? ? ? ? ?// 1、先在應用本地目錄下查找類?
? ? ? ? ? ? ? ? ? ? clazz = findClassInternal(name);
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }?catch(AccessControlException ace) {
? ? ? ? ? ? ? ? log.warn("WebappClassLoader.findClassInternal("?+ name
? ? ? ? ? ? ? ? ? ? ? ? +?") security exception: "?+ ace.getMessage(), ace);
? ? ? ? ? ? ? ??throw?new?ClassNotFoundException(name, ace);
? ? ? ? ? ? }?catch?(RuntimeException e) {
? ? ? ? ? ? ? ??if?(log.isTraceEnabled())
? ? ? ? ? ? ? ? ? ? log.trace(" ? ? ?-->RuntimeException Rethrown", e);
? ? ? ? ? ? ? ??throw?e;
? ? ? ? ? ? }
? ? ? ? ? ??if?((clazz ==?null) && hasExternalRepositories) {
? ? ? ? ? ? ? ??try?{
? ? ? ? ? ? ? ? ? ? ?// 2、如果在本地目錄沒有找到,委派父加載器去查找
? ? ? ? ? ? ? ? ? ? clazz =?super.findClass(name);
? ? ? ? ? ? ? ? }?catch(AccessControlException ace) {
? ? ? ? ? ? ? ? ? ? log.warn("WebappClassLoader.findClassInternal("?+ name
? ? ? ? ? ? ? ? ? ? ? ? ? ? +?") security exception: "?+ ace.getMessage(), ace);
? ? ? ? ? ? ? ? ? ??throw?new?ClassNotFoundException(name, ace);
? ? ? ? ? ? ? ? }?catch?(RuntimeException e) {
? ? ? ? ? ? ? ? ? ??if?(log.isTraceEnabled())
? ? ? ? ? ? ? ? ? ? ? ? log.trace(" ? ? ?-->RuntimeException Rethrown", e);
? ? ? ? ? ? ? ? ? ??throw?e;
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? ? ??// 3、如果父加載器也沒找到,拋出異常
? ? ? ? ? ??if?(clazz ==?null) {
? ? ? ? ? ? ? ??if?(log.isDebugEnabled())
? ? ? ? ? ? ? ? ? ? log.debug(" ? ?--> Returning ClassNotFoundException");
? ? ? ? ? ? ? ??throw?new?ClassNotFoundException(name);
? ? ? ? ? ? }
? ? ? ? }?catch?(ClassNotFoundException e) {
? ? ? ? ? ??if?(log.isTraceEnabled())
? ? ? ? ? ? ? ? log.trace(" ? ?--> Passing on ClassNotFoundException");
? ? ? ? ? ??throw?e;
? ? ? ? }
? ? ? ??// Return the class we have located
? ? ? ??if?(log.isTraceEnabled())
? ? ? ? ? ? log.debug(" ? ? ?Returning class "?+ clazz);
? ? ? ??if?(log.isTraceEnabled()) {
? ? ? ? ? ? ClassLoader cl;
? ? ? ? ? ??if?(Globals.IS_SECURITY_ENABLED){
? ? ? ? ? ? ? ? cl = AccessController.doPrivileged(
? ? ? ? ? ? ? ? ? ??new?PrivilegedGetClassLoader(clazz));
? ? ? ? ? ? }?else?{
? ? ? ? ? ? ? ? cl = clazz.getClassLoader();
? ? ? ? ? ? }
? ? ? ? ? ? log.debug(" ? ? ?Loaded by "?+ cl.toString());
? ? ? ? }
? ? ? ??return?(clazz);
? ? }
???????
@Override
public?Class<?> loadClass(String name,?boolean?resolve)?throws?ClassNotFoundException {
? ??synchronized?(getClassLoadingLock(name)) {
? ? ? ? Class<?> clazz =?null;
? ? ? ??// 1、從本地緩存中查找是否加載過此類
? ? ? ? clazz = findLoadedClass0(name);
? ? ? ??if?(clazz !=?null) {
? ? ? ? ? ??if?(log.isDebugEnabled())
? ? ? ? ? ? ? ? log.debug(" ?Returning class from cache");
? ? ? ? ? ??if?(resolve)
? ? ? ? ? ? ? ? resolveClass(clazz);
? ? ? ? ? ??return?clazz;
? ? ? ? }
? ? ? ??// 2、從AppClassLoader中查找是否加載過此類
? ? ? ? clazz = findLoadedClass(name);
? ? ? ??if?(clazz !=?null) {
? ? ? ? ? ??if?(log.isDebugEnabled())
? ? ? ? ? ? ? ? log.debug(" ?Returning class from cache");
? ? ? ? ? ??if?(resolve)
? ? ? ? ? ? ? ? resolveClass(clazz);
? ? ? ? ? ??return?clazz;
? ? ? ? }
? ? ? ??String?resourceName?=?binaryNameToPath(name,?false);
? ? ? ??// 3、嘗試用ExtClassLoader 類加載器加載類,防止應用覆蓋JRE的核心類
? ? ? ??ClassLoader?javaseLoader?=?getJavaseClassLoader();
? ? ? ??boolean?tryLoadingFromJavaseLoader;
? ? ? ??try?{
? ? ? ? ? ? URL url;
? ? ? ? ? ??if?(securityManager !=?null) {
? ? ? ? ? ? ? ? PrivilegedAction<URL> dp =?new?PrivilegedJavaseGetResource(resourceName);
? ? ? ? ? ? ? ? url = AccessController.doPrivileged(dp);
? ? ? ? ? ? }?else?{
? ? ? ? ? ? ? ? url = javaseLoader.getResource(resourceName);
? ? ? ? ? ? }
? ? ? ? ? ? tryLoadingFromJavaseLoader = (url !=?null);
? ? ? ? }?catch?(Throwable t) {
? ? ? ? ? ? tryLoadingFromJavaseLoader =?true;
? ? ? ? }
? ? ? ??boolean?delegateLoad?=?delegate || filter(name,?true);
? ? ? ??// 4、判斷是否設置了delegate屬性,如果設置為true那么就按照雙親委派機制加載類
? ? ? ??if?(delegateLoad) {
? ? ? ? ? ??if?(log.isDebugEnabled())
? ? ? ? ? ? ? ? log.debug(" ?Delegating to parent classloader1 "?+ parent);
? ? ? ? ? ??try?{
? ? ? ? ? ? ? ? clazz = Class.forName(name,?false, parent);
? ? ? ? ? ? ? ??if?(clazz !=?null) {
? ? ? ? ? ? ? ? ? ??if?(log.isDebugEnabled())
? ? ? ? ? ? ? ? ? ? ? ? log.debug(" ?Loading class from parent");
? ? ? ? ? ? ? ? ? ??if?(resolve)
? ? ? ? ? ? ? ? ? ? ? ? resolveClass(clazz);
? ? ? ? ? ? ? ? ? ??return?clazz;
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }?catch?(ClassNotFoundException e) {
? ? ? ? ? ? ? ??// Ignore
? ? ? ? ? ? }
? ? ? ? }
? ? ? ??// 5、默認是設置delegate是false的,那么就會先用WebAppClassLoader進行加載
? ? ? ??if?(log.isDebugEnabled())
? ? ? ? ? ? log.debug(" ?Searching local repositories");
? ? ? ??try?{
? ? ? ? ? ? clazz = findClass(name);
? ? ? ? ? ??if?(clazz !=?null) {
? ? ? ? ? ? ? ??if?(log.isDebugEnabled())
? ? ? ? ? ? ? ? ? ? log.debug(" ?Loading class from local repository");
? ? ? ? ? ? ? ??if?(resolve)
? ? ? ? ? ? ? ? ? ? resolveClass(clazz);
? ? ? ? ? ? ? ??return?clazz;
? ? ? ? ? ? }
? ? ? ? }?catch?(ClassNotFoundException e) {
? ? ? ? ? ??// Ignore
? ? ? ? }
? ? ? ??// 6、如果在WebAppClassLoader沒找到類,那么就委托給AppClassLoader去加載
? ? ? ??if?(!delegateLoad) {
? ? ? ? ? ??if?(log.isDebugEnabled())
? ? ? ? ? ? ? ? log.debug(" ?Delegating to parent classloader at end: "?+ parent);
? ? ? ? ? ??try?{
? ? ? ? ? ? ? ? clazz = Class.forName(name,?false, parent);
? ? ? ? ? ? ? ??if?(clazz !=?null) {
? ? ? ? ? ? ? ? ? ??if?(log.isDebugEnabled())
? ? ? ? ? ? ? ? ? ? ? ? log.debug(" ?Loading class from parent");
? ? ? ? ? ? ? ? ? ??if?(resolve)
? ? ? ? ? ? ? ? ? ? ? ? resolveClass(clazz);
? ? ? ? ? ? ? ? ? ??return?clazz;
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }?catch?(ClassNotFoundException e) {
? ? ? ? ? ? ? ??// Ignore
? ? ? ? ? ? }
? ? ? ? }
? ? }
? ??throw?new?ClassNotFoundException(name);
}
先在本地緩存中查找該類是否已經加載過,如果加載過就返回緩存中的。
如果沒有加載過,委托給AppClassLoader是否加載過,如果加載過就返回。
如果AppClassLoader也沒加載過,委托給ExtClassLoader去加載,這么做的目的就是:
防止應用自己的類庫覆蓋了核心類庫,因為WebAppClassLoader需要打破雙親委托機制,假如應用里自定義了一個叫java.lang.String的類,如果先加載這個類,就會覆蓋核心類庫的java.lang.String,所以說它會優先嘗試用ExtClassLoader去加載,因為ExtClassLoader加載不到同樣也會委托給BootstrapClassLoader去加載,也就避免了覆蓋了核心類庫的問題。
如果ExtClassLoader也沒有查找到,說明核心類庫中沒有這個類,那么就在本地應用目錄下查找此類并加載。
如果本地應用目錄下還有沒有這個類,那么肯定不是應用自己定義的類,那么就由AppClassLoader去加載。
這里是通過Class.forName()調用AppClassLoader類加載器的,因為Class.forName()的默認加載器就是AppClassLoader。
如果上述都沒有找到,那么只能拋出ClassNotFoundException了。
疑惑點解釋:
正常情況下(如?AppClassLoader
):
加載類時先問父加載器(向上委托),父加載器找不到才自己加載。
Tomcat 的?WebAppClassLoader
?不完全遵守這個規則:
- 優先自己加載
(應用目錄下的類),而不是先問父加載器。
- 只有核心類(如?
java.*
)才會向上委托防止應用覆蓋 JDK 的類。
目的主要是保護核心類,遇到?java.*
?等核心類名時,仍向上委托,防止篡改 JDK。
四、垃圾回收
什么是Java里的垃圾回收?如何觸發垃圾回收?
垃圾回收(Garbage Collection, GC)是自動管理內存的一種機制,它負責自動釋放不再被程序引用的對象所占用的內存,這種機制減少了內存泄漏和內存管理錯誤的可能性。垃圾回收可以通過多種方式觸發,具體如下:
內存不足時:當JVM檢測到堆內存不足,無法為新的對象分配內存時,會自動觸發垃圾回收。
手動請求:雖然垃圾回收是自動的,開發者可以通過調用?
System.gc()
?或
Runtime.getRuntime().gc()
?建議 JVM 進行垃圾回收。不過這只是一個建議,并不能保證立即執行。
JVM參數:啟動 Java 應用時可以通過 JVM 參數來調整垃圾回收的行為,比如:
-Xmx
(最大堆大小)、-Xms
(初始堆大小)等。對象數量或內存使用達到閾值:垃圾收集器內部實現了一些策略,以監控對象的創建和內存使用,達到某個閾值時觸發垃圾回收。
怎么判斷垃圾?
在Java中,判斷對象是否為垃圾(即不再被使用,可以被垃圾回收器回收)主要依據兩種主流的垃圾回收算法來實現:引用計數法和可達性分析算法。
引用計數法(Reference Counting)
原理:為每個對象分配一個引用計數器,每當有一個地方引用它時,計數器加1;當引用失效時,計數器減1。當計數器為0時,表示對象不再被任何變量引用,可以被回收。
缺點:不能解決循環引用的問題,即兩個對象相互引用,但不再被其他任何對象引用,這時引用計數器不會為0,導致對象無法被回收。
可達性分析算法(Reachability Analysis)
Java虛擬機主要采用此算法來判斷對象是否為垃圾。
原理:從一組稱為GC Roots(垃圾收集根)的對象出發,向下追溯它們引用的對象,以及這些對象引用的其他對象,以此類推。如果一個對象到GC Roots沒有任何引用鏈相連(即從GC Roots到這個對象不可達),那么這個對象就被認為是不可達的,可以被回收。GC Roots對象包括:虛擬機棧(棧幀中的本地變量表)中引用的對象、方法區中類靜態屬性引用的對象、本地方法棧中JNI(Java Native Interface)引用的對象、活躍線程的引用等。