虛擬機運行快速復習
try-catch:catch-異常表+棧展開,finally-代碼復制+異常表兜底
類的生命周期:加載,連接(驗證,準備,解析),初始化,使用,卸載
類加載器:加載字節碼.Class到JVM中生成一個Class對象
大部分類在具體用到的時候才會去加載(懶加載機制),已經加載的類會被放在 ClassLoader
中
對于一個類加載器來說,相同二進制名稱的類只會被加載一次
類加載過程:
加載:通過類的全限定名獲取該類的二進制字節流,存到類常量池,內存中生成Class對象
連接:
- 驗證:驗證Class二進制字節流合規
- 準備:為類對象分配內存
- 解析:符號引用轉為直接引用
初始化:執行初始化方法
對象創建過程:類加載檢查+分配內存+初始化零值+設置對象頭+執行對象初始化方法
類加載器:
啟動類加載器|
拓展類加載器|
應用程序類加載器|
自定義類加載器
因為啟動類加載器是C++做的,所以獲取到ClassLoader為NULL的時候就是啟動類加載器
自定義類加載器:
需要繼承 ClassLoader
抽象類
不打破雙親委派機制:重寫 ClassLoader
類中的 findClass()
打破雙親委派機制:重寫 loadClass()
方法
類卸載:類無用的時候卸載
無用的類:所有實例被回收,類加載器被回收,Class對象沒有任何引用
雙親委派機制:從下往上判斷類是否被加載,從上往下嘗試加載類
JVM 判定兩個 Java 類是否相同的具體規則:全限定類名+類加載器
為什么要用雙親委派機制:可以避免類被重復加載,同時保證我們的核心API不被修改
如何實現熱部署:自定義類加載器,并重寫 ClassLoader 的 loadClass() ?法
熱部署三步:
1)銷毀原來的?定義 ClassLoader
2)更新 class 類?件
3)創建新的 ClassLoader 去加載更新后的 class 類?件
try-catch在jvm層面是怎么做的?
java中的try-catch通過異常表和棧展開來實現
異常表(exception-table)
每個方法的字節碼中都有一個異常表,用于記錄try-catch塊的作用范圍和對應的異常處理邏輯
異常表的每個條目包含以下信息:
起點,終點,處理代碼的位置,捕獲異常的類型
起點(start_pc):try塊的起始指令偏移量
終點(end_pc):try塊的結束指令偏移量(不包含該指令)
處理代碼位置(handler_pc):catch塊的第一條指令偏移量
捕獲的異常類型(catch_type):要捕獲的異常類(如java/lang/Exception),若為0表示捕獲所有異常(finally塊)
字節碼結構
Exception table:start end handler type0 10 13 java/io/IOException0 10 20 java/lang/Exception
異常處理流程
拋出異常,從異常表判斷異常是否在處理邏輯內(也就是是否被try-catch{}包圍),
代碼中拋出異常時,JVM會執行以下步驟:
創建異常對象:實例化拋出的異常(如new IOException()
)
查找異常表:從當前方法的異常表中,按順序匹配以下條件:
異常拋出的位置是否在某個條目的[start_pc, end_pc)
范圍內。
拋出的異常是否是catch_type
的子類(或自身)
跳轉到處理代碼:
若找到匹配條目,跳轉到handler_pc
執行catch
塊
若未找到,觸發棧展開:彈出當前棧幀,回到調用者方法重復上述過程。棧展開確保異常沿調用鏈向上傳播,直到被處理或終止線程
未捕獲異常:若所有棧幀均未處理異常,線程終止并打印堆棧跟蹤
finally塊的實現
finally 塊的核心是:無論 try 或 catch 塊中是否拋出異常或提前返回,finally 中的代碼必須執行
為了實現這一點,JVM 的編譯器(如 javac)在生成字節碼時,會通過兩種機制來確保 finally 的執行
finally塊通過兩種方式實現:
代碼復制:編譯器將finally
代碼復制到try
和catch
塊的所有退出路徑(包括return
或異常拋出之后)。
異常表條目兜底:若finally
需要處理異常退出,會生成一個catch_type=0
的條目,捕獲所有異常并執行finally
代碼,之后重新拋出異常
代碼復制
編譯器會將 finally 塊中的代碼復制到所有可能的退出路徑,包括:
try 塊正常結束后的退出路徑。
catch 塊處理完異常后的退出路徑。
try 或 catch 塊中的 return、break、continue 語句之前
java代碼
public void example() {try {System.out.println("try");} catch (Exception e) {System.out.println("catch");} finally {System.out.println("finally");}
}
編譯后的字節碼邏輯
// try 塊
L0:System.out.println("try");// 復制 finally 代碼到 try 塊末尾System.out.println("finally");return;// catch 塊
L1:System.out.println("catch");// 復制 finally 代碼到 catch 塊末尾System.out.println("finally");return;// 異常表條目(自動處理異常后的 finally)
Exception table:start=L0, end=L0, handler=L1, type=Exception
關鍵點:
finally 的代碼會被復制到 try 和 catch 的末尾,確保正常流程下一定會執行
如果 try 或 catch 中有 return,編譯器會先執行 finally 代碼,再執行 return
異常表兜底(處理未捕獲的異常)
如果 try 或 catch 塊中拋出了未被捕獲的異常,或者有 throw 語句,JVM 會通過異常表跳轉到 finally 代碼,執行后再重新拋出異常。
異常表條目
編譯器會生成一個特殊的異常表條目,用于捕獲所有類型的異常(catch_type=0)
確保任何未處理的異常都會先執行 finally,再繼續傳播異常
java代碼
public void example() {try {throw new IOException();} finally {System.out.println("finally");}
}
字節碼的異常表會生成如下頭目
Exception table:start=L0, end=L1, handler=L2, type=0 // type=0 表示捕獲所有異常
對應的執行流程:
try
塊拋出IOException
。- JVM 查找異常表,發現
type=0
的條目(匹配所有異常)。 - 跳轉到
handler=L2
(finally
代碼的位置)執行System.out.println("finally")
。 - 重新拋出異常,繼續棧展開
假設代碼中有 try 和 finally,但沒有 catch:
try {throw new Exception();
} finally {System.out.println("finally");
}
執行步驟:
try
塊拋出異常,JVM 創建異常對象。- 直接查找當前方法的異常表,找到
catch_type=0
的條目,跳轉到finally
代碼。 - 執行
finally
塊中的代碼。 - 重新拋出異常,由外層調用者處理
簡單總結
異常處理:異常表+棧展開
每個方法的字節碼中都有一個異常表,用于記錄try-catch塊的作用范圍和對應的異常處理邏輯
記錄
try的起點
try的終點
catch的位置(處理代碼的位置)
捕獲的異常類型
當出現異常的時候查找異常表,查看異常出現的位置,如果有try-catch,就跳轉到catch進行處理
沒有的話就進行棧展開,棧展開確保異常沿調用鏈向上傳播,直到被處理或終止線程
finally塊通過代碼復制和異常表兜底,保證finally塊必須執行
代碼復制:
編譯器將finally
代碼復制到try
和catch
塊的所有退出路徑(包括return
或異常拋出之后)
異常表兜底:
如果 try 或 catch 塊中拋出了未被捕獲的異常,或者有 throw 語句,JVM 會通過異常表跳轉到 finally 代碼,執行后再重新拋出異常
編譯器會生成一個特殊的異常表條目,用于捕獲所有類型的異常(catch_type=0)
確保任何未處理的異常都會先執行 finally,再繼續傳播異常
能說一下類的生命周期嗎
?個類從被加載到虛擬機內存中開始,到從內存中卸載,整個?命周期需要經過七個階段
加載 (Loading)
連接:{
驗證(Verification)、
準備(Preparation)、
解析(Resolution)、
}
初始化 (Initialization)
使?(Using)
卸載(Unloading)
什么是類加載器
類加載器是一個負責加載類的對象,用于實現類加載過程中的加載這一步
1.每個 Java 類都有一個引用指向加載它的 ClassLoader
2.數組類不是通過 ClassLoader 創建的(數組類沒有對應的二進制字節流),是由 JVM 直接生成
簡單來說,類加載器的主要作用就是加載 Java 類的字節碼( .class 文件)到 JVM 中(在內存中生成一個代表該類的 Class 對象)
字節碼可以是 Java 源程序(.java文件)經過 javac 編譯得來,也可以是通過工具動態生成或者通過網絡下載得來
其實除了加載類之外,類加載器還可以加載 Java 應用所需的資源如文本、圖像、配置文件、視頻等等文件資源。本文只討論其核心功能:加載類
類加載器是動態加載還是靜態加載
JVM 啟動的時候,并不會一次性加載所有的類,而是根據需要去動態加載
也就是說,大部分類在具體用到的時候才會去加載(懶加載機制),這樣對內存更加友好
對于已經加載的類會被放在 ClassLoader
中
在類加載的時候,系統會首先判斷當前類是否被加載過,已經被加載的類會直接返回,否則才會嘗試加載
也就是說,對于一個類加載器來說,相同二進制名稱的類只會被加載一次
類加載的過程知道嗎
?
加載是 JVM 加載的起點,具體什么時候開始加載,《Java 虛擬機規范》中并沒有進?強制約束,可以交給虛擬機 的具體實現來?由把握。
類加載過程:加載,連接{驗證,準備,解析},初始化
加載
加載過程JVM 要做三件事情:
1)通過?個類的全限定名來獲取定義此類的?進制字節流。
2)將這個字節流所代表的靜態存儲結構轉化為方法區(因為包含類常量池)的運行時數據結構
3)在內存中?成?個代表這個類的 java.lang.Class 對象,作為?法區這個類的各種數據的訪問入口
加載階段結束后,Java 虛擬機外部的?進制字節流就按照虛擬機所設定的格式存儲在?法區之中了,?法區中的數據存儲格式完全由虛擬機實現??定義,《Java 虛擬機規范》未規定此區域的具體數據結構。
類型數據妥善安置在?法區之后,會在 Java 堆內存中實例化?個 java.lang.Class 類的對象, 這個對象將作為程序訪問?法區中的類型數據的外部接口
連接
驗證(驗證合法)
驗證是連接階段的第一步,這一階段的目的是確保 Class 文件的字節流中包含的信息符合《Java 虛擬機規范》的全部約束要求,保證這些信息被當作代碼運行后不會危害虛擬機自身的安全
準備(分配內存)
準備階段是正式為類變量分配內存并設置類變量初始值的階段,這些內存都將在方法區中分配
解析(符號引用轉直接引用)
解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程
初始化
初始化階段是執行初始化方法 <clinit> ()
方法的過程,是類加載的最后一步,這一步 JVM 才開始真正執行類中定義的 Java 程序代碼(字節碼)
類加載總結
JVM 中內置了三個重要的 ClassLoader
:
BootstrapClassLoader
(啟動類加載器):最頂層的加載類,由 C++實現,通常表示為 null,并且沒有父級,主要用來加載 JDK 內部的核心類庫(%JAVA_HOME%/lib
目錄下的rt.jar
、resources.jar
、charsets.jar
等 jar 包和類)以及被-Xbootclasspath
參數指定的路徑下的所有類。ExtensionClassLoader
(擴展類加載器):主要負責加載%JRE_HOME%/lib/ext
目錄下的 jar 包和類以及被java.ext.dirs
系統變量所指定的路徑下的所有類。AppClassLoader
(應用程序類加載器):面向我們用戶的加載器,負責加載當前應用 classpath 下的所有 jar 包和類
除了這三種類加載器之外,用戶還可以加入自定義的類加載器來進行拓展,以滿足自己的特殊需求。就比如說,我們可以對 Java 類的字節碼( .class
文件)進行加密,加載時再利用自定義的類加載器對其解密
除了 BootstrapClassLoader
是 JVM 自身的一部分之外,其他所有的類加載器都是在 JVM 外部實現的,并且全都繼承自 ClassLoader
抽象類。這樣做的好處是用戶可以自定義類加載器,以便讓應用程序自己決定如何去獲取所需的類
為什么 獲取到 ClassLoader
為null
就是 BootstrapClassLoader
加載的呢?
這是因為BootstrapClassLoader
由 C++ 實現,由于這個 C++ 實現的類加載器在 Java 中是沒有與之對應的類的,所以拿到的結果是 null
類加載器有哪些?
主要有四種類加載器:
- 啟動類加載器(Bootstrap ClassLoader)?來加載 java 核?類庫,?法被 java 程序直接引?。
- 擴展類加載器(extensions class loader):它?來加載 Java 的擴展庫。Java 虛擬機的實現會提供?個擴展庫?錄。該類加載器在此?錄??查找并加載 Java 類。
- 系統類加載器(system class loader):它根據 Java 應?的類路徑(CLASSPATH)來加載 Java 類。?般來說,Java 應?的類都是由它來完成加載的。可以通ClassLoader.getSystemClassLoader()來獲取它。
- 用戶自定義類加載器 (user class loader),?戶通過繼承 java.lang.ClassLoader 類的?式??實現的類加載器
如何自定義類加載器?
我們前面也說說了,除了 BootstrapClassLoader
其他類加載器均由 Java 實現且全部繼承自java.lang.ClassLoader
。如果我們要自定義自己的類加載器,很明顯需要繼承 ClassLoader
抽象類
ClassLoader
類有兩個關鍵的方法:
protected Class loadClass(String name, boolean resolve)
:加載指定二進制名稱的類,實現了雙親委派機制 。name
為類的二進制名稱,resolve
如果為 true,在加載時調用resolveClass(Class<?> c)
方法解析該類。protected Class findClass(String name)
:根據類的二進制名稱來查找類,默認實現是空方法。
官方 API 文檔中寫到:
Subclasses of ClassLoader
are encouraged to override findClass(String name)
, rather than this method.
建議 ClassLoader
的子類重寫 findClass(String name)
方法而不是loadClass(String name, boolean resolve)
方法
如果我們不想打破雙親委派模型:
需要重寫 ClassLoader
類中的 findClass()
方法即可,無法被父類加載器加載的類最終會通過這個方法被加載
如果我們想打破雙親委派模型:
需要重寫 loadClass()
方法
說一下類卸載
卸載類即該類的 Class 對象被 GC。
卸載類需要滿足 3 個要求:
- 該類的所有的實例對象都已被 GC,也就是說堆不存在該類的實例對象。
- 該類沒有在其他任何地方被引用
- 該類的類加載器的實例已被 GC
所以,在 JVM 生命周期內,由 jvm 自帶的類加載器加載的類是不會被卸載的
但是由我們自定義的類加載器加載的類是可能被卸載的
只要想通一點就好了,JDK 自帶的 BootstrapClassLoader
, ExtClassLoader
, AppClassLoader
負責加載 JDK 提供的類,所以它們(類加載器的實例)肯定不會被回收。而我們自定義的類加載器的實例是可以被回收的,所以使用我們自定義加載器加載的類是可以被卸載掉的
什么是雙親委派機制?
雙親委派模型的工作過程
雙親委派模型的?作過程:如果?個類加載器收到了類加載的請求,它?先不會??去嘗試加載這個類,?是把這個請求委派給父類加載器去完成,每?個層次的類加載器都是如此
因此所有的加載請求最終都應該傳送到最頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載請求時,子加載器才會嘗試自己去完成加載
雙親委派模型介紹
類加載器有很多種,當我們想要加載一個類的時候,具體是哪個類加載器加載呢?這就需要提到雙親委派模型了
ClassLoader
類使用委托模型來搜索類和資源。- 雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應有自己的父類加載器。
ClassLoader
實例會在試圖親自查找類或資源之前,將搜索類或資源的任務委托給其父類加載器
下圖展示的各種類加載器之間的層次關系被稱為類加載器的“雙親委派模型(Parents Delegation Model)”
從下往上判斷類是否被加載
從上往下嘗試加載類
?
說一下雙親委派模型的執行流程?
簡單總結一下雙親委派模型的執行流程:
1.在類加載的時候,系統會首先判斷當前類是否被加載過。已經被加載的類會直接返回,否則才會嘗試加載(每個父類加載器都會走一遍這個流程)。
2.類加載器在進行類加載的時候,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成(調用父加載器 loadClass()
方法來加載類)。這樣的話,所有的請求最終都會傳送到頂層的啟動類加載器 BootstrapClassLoader
中。
3.只有當父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載(調用自己的 findClass()
方法來加載類)。
4.如果子類加載器也無法加載這個類,那么它會拋出一個 ClassNotFoundException
異常
拓展一下:
JVM 判定兩個 Java 類是否相同的具體規則:JVM 不僅要看類的全名是否相同,還要看加載此類的類加載器是否一樣。只有兩者都相同的情況,才認為兩個類是相同的。即使兩個類來源于同一個 Class
文件,被同一個虛擬機加載,只要加載它們的類加載器不同,那這兩個類就必定不相同。
為什么要用雙親委派機制
?
答案是為了保證應?程序的穩定有序。
例如類 java.lang.Object,它存放在 rt.jar 之中,通過雙親委派機制,保證最終都是委派給處于模型最頂端的啟動類加載器進?加載,保證 Object 的?致。
反之,都由各個類加載器??去加載的話,如果?戶??也編寫了?個名為 java.lang.Object 的類,并放在程序的 ClassPath 中,那系統中就會出現多個不同的 Object 類
可以避免類被重復加載,同時保證我們的核心API不被修改
說一下雙親委派模型的好處
雙親委派模型保證了 Java 程序的穩定運行,可以避免類的重復加載(JVM 區分不同類的方式不僅僅根據類名,相同的類文件被不同的類加載器加載產生的是兩個不同的類)
保證了 Java 的核心 API 不被篡改。
如果沒有使用雙親委派模型,而是每個類加載器加載自己的話就會出現一些問題:
比如我們編寫一個稱為 java.lang.Object
類的話,那么程序運行的時候,系統就會出現兩個不同的 Object
類。雙親委派模型可以保證加載的是 JRE 里的那個 Object
類,而不是你寫的 Object
類。
這是因為 AppClassLoader
在加載你的 Object
類時,會委托給 ExtClassLoader
去加載,而 ExtClassLoader
又會委托給 BootstrapClassLoader
,BootstrapClassLoader
發現自己已經加載過了 Object
類,會直接返回,不會去加載你寫的 Object
類
可以避免類被重復加載,同時保證我們的核心API不被修改
如何破打破雙親委派機制?
?
自定義加載器的話,需要繼承 ClassLoader
。
如果我們不想打破雙親委派模型,就重寫 ClassLoader
類中的 findClass()
方法即可,無法被父類加載器加載的類最終會通過這個方法被加載。
但是,如果想打破雙親委派模型則需要重寫 loadClass()
方法
?
我們有什么場景需要破壞我們的雙親委派機制
?
自定義類場景
破壞雙親委派機制的場景
1. 動態加載和熱更新:
在某些開發環境中,可能需要動態加載新的類或更新現有類。為了實現熱更新,可能需要創建一個自定義的類加載器,繞過雙親委派機制,以便直接加載新的類定義。
2. 插件架構:
在插件系統中,可能需要允許插件直接使用特定的類,而這些類可能與主應用程序中的類同名。通過自定義類加載器,可以避免加載主應用程序中的同名類,從而實現插件的獨立性。
3. 隔離不同版本的庫:
有時,應用程序可能需要同時使用同一庫的不同版本。通過創建不同的類加載器,可以加載不同版本的庫而不發生沖突,從而破壞雙親委派機制。
4. 安全性需求:
在某些安全敏感的應用場景中,可能需要自定義類加載器以實現更嚴格的安全控制。例如,可以限制某些類的加載,或加載特定來源的類。
5. 測試和調試:
在單元測試或調試過程中,可能需要加載特定版本的類或模擬某些類的行為。自定義類加載器可以幫助實現這種需求
你覺得應該怎么實現一個熱部署功能
?
Java類的加載過程
我們已經知道了 Java 類的加載過程。?個 Java 類?件到虛擬機?的對象,要經過如下過程:
?先通過 Java 編譯器,將 Java ?件編譯成 class 字節碼,
類加載器讀取 class 字節碼,再將類轉化為實例,
對實例 newInstance 就可以?成對象。
類加載器 ClassLoader 的功能
也就是將 class 字節碼轉換到類的實例。在 Java 應?中,所有的實例都是由類加載器加載而來。
?般在系統中,類的加載都是由系統?帶的類加載器完成,
?且對于同?個全限定名的 java 類(如 com.csiar.soc.HelloWorld)
只能被加載?次,而且無法被卸載
這個時候問題就來了,如果我們希望將 java 類卸載,并且替換更新版本的 java 類,該怎么做呢?
既然在類加載器中,Java 類只能被加載?次,并且?法卸載。
那么我們是不是可以直接把 Java 類加載器干掉呢?
答案是可以的
自定義類加載器
我們可以?定義類加載器,并重寫 ClassLoader 的 findClass ?法。
想要實現熱部署可以分以下三個步驟:
1)銷毀原來的?定義 ClassLoader
2)更新 class 類?件
3)創建新的 ClassLoader 去加載更新后的 class 類?件。
到此,?個熱部署的功能就這樣實現了
Tomcat 的類加載機制了解嗎
Tomcat 是主流的 Java Web 服務器之?,為了實現?些特殊的功能需求,?定義了?些類加載器。
Tomcat 類加載器如下:
Tomcat 實際上也是破壞了雙親委派模型的。
Tomact 是 web 容器,可能需要部署多個應?程序。不同的應?程序可能會依賴同?個第三?類庫的不同版本,但是不同版本的類庫中某?個類的全路徑名可能是?樣的。如多個應?都要依賴 hollis.jar,但是 A 應?需要依賴1.0.0 版本,但是 B 應?需要依賴 1.0.1 版本。這兩個版本中都有?個類是 com.hollis.Test.class。如果采?默認的雙親委派類加載機制,那么?法加載多個相同的類。
所以,Tomcat 破壞了雙親委派原則,提供隔離的機制,為每個 web 容器單獨提供?個 WebAppClassLoader 加載器。每?個 WebAppClassLoader 負責加載本身的?錄下的 class ?件,加載不到時再交 CommonClassLoader加載,這和雙親委派剛好相反