JVM是如何處理異常的
上篇博客我們簡單說過異常信息是存放在屬性表集合中的Code屬性表里
,那么這篇博客就單獨講Code屬性表中的exception_table。
在講之前我們先思考兩個問題?
1、為什么捕獲異常會較大的性能消耗?
2、為什么finally中的代碼會永遠執行?
接下來會從JVM虛擬機的角度來解答這兩個問題。
一、概念
1、JVM是如何捕獲異常的?
1、編譯而成的字節碼中,每個方法都附帶一個異常表
。
2、異常表中每一個條目代表一個異常處理器
3、觸發異常時,JVM會遍歷異常表,比較觸發異常的字節碼的索引值是否在異常處理器的from指針到to指針
的范圍內。
4、范圍匹配后,會去比較異常類型和異常處理器中的type是否相同
。
5、類型匹配后,會跳轉到target指針所指向的字節碼
(catch代碼塊的開始位置)
6、如果沒有匹配到異常處理器,會彈出當前方法對應的Java棧幀
,並對調用者重復上述操作。
2、什么是異常表?
1. 每個方法都附帶一個異常表
2. 異常表中每一個條目, 就是一個異常處理器
異常表如下:

3、什么是異常處理器?其組成部分有哪些?
1、異常處理器由from指針、to指針、target指針,以及所捕獲的異常類型所構成(type)。
2、這些指針的數值就是字節碼的索引(bytecode index, bci),可以直接去定位字節碼。
3、from指針和to指針,標識了該異常處理器所監控的返回
4、target指針,指向異常處理器的起始位置。如catch代碼塊的起始位置
5、type:捕獲的異常類型,如Exception
4、如果在方法的異常表中沒有匹配到異常處理器,會怎么樣?
1、會彈出當前方法對應的Java棧幀
2、在調用者上重復異常匹配的流程。
3、最壞情況下,JVM需要編譯當前線程Java棧上所有方法的異常表
二、代碼演示
1、try-catch
public static void main(String[] args) {
try {
mayThrowException();
} catch (Exception e) {
e.printStackTrace();
}
}
// 對應的 Java 字節碼
public static void main(java.lang.String[]);
Code:
0: invokestatic mayThrowException:()V
3: goto 11
6: astore_1
7: aload_1
8: invokevirtual java.lang.Exception.printStackTrace
11: return
Exception table:
from to target type
0 3 6 Class java/lang/Exception // 異常表條目
上面Code中的字節碼自己也沒有仔細研究過,我們可以具體看下面的Exception table表,來進行分析。
1、from和to
: 指是try和catch之間的代碼的索引位置。from=0,to=3,代表從字節索引0的位置到3(不包括3)。
2、target
: 代表catch后代碼運行的起始位置。
3、type
: 指的是異常類型,這里是指Exception異常。
當程序觸發異常時,java虛擬機會從上至下遍歷異常表中的所有條目。當觸發異常的字節碼的索引值在某個異常表條目的監控范圍內,Java 虛擬機會判斷所拋出的異常
和該條目想要捕獲的異常是否匹配。如果匹配,Java 虛擬機會將控制流轉移至該條目target 指針指向的字節碼。
如果遍歷完所有異常表條目,Java 虛擬機仍未匹配到異常處理器,那么它會彈出當前方法對應的Java 棧幀
,並且在調用者(caller)中重復上述操作。在最壞情況下,
Java 虛擬機需要遍歷當前線程 Java 棧上所有方法的異常表。
2、try-catch-finally
finally 代碼塊的編譯比較復雜。當前版本 Java 編譯器的做法,是復制 finally 代碼塊的內容,分別放在 try-catch 代碼塊所有正常執行路徑以及異常執行路徑的出口中
。

代碼示例
public static void XiaoXiao() {
try {
dada();
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("Finally");
}
}
//通過javap 反編譯
public static void XiaoXiao();
Code:
0: invokestatic #3 // Method dada:()V
3: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
6: ldc #7 // String Finally
8: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
11: goto 41
14: astore_0
15: aload_0
16: invokevirtual #5 // Method java/lang/Exception.printStackTrace:()V
19: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
22: ldc #7 // String Finally
24: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
27: goto 41
30: astore_1
31: getstatic #6 // Field java/lang/System.out:Ljava/io/PrintStream;
34: ldc #7 // String Finally
36: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
39: aload_1
40: athrow
41: return
Exception table:
from to target type
0 3 14 Class java/lang/Exception
0 3 30 any
14 19 30 any
和之前有所不同,這次
1、異常表中,有三條數據,而我們僅僅捕獲了一個Exception
2、異常表的后兩個item的type為any
上面的三條異常表item的意思為
1、如果0到3之間,發生了Exception類型的異常,調用14位置的異常處理者。
2、 如果0到3之間,無論發生什么異常,都調用30位置的處理者。
3、 如果14到19之間(即catch部分),不論發生什么異常,都調用30位置的處理者。
`問題`:通過上面那幅圖和javap反編譯代碼就可以很好的解釋為什么finally里面的代碼永遠會執行?
原因:因為當前版本Java編譯器的做法,是復制finally代碼塊的內容,分別放到所有正常執行路徑,以及異常執行路徑的出口中
。
這三份finally代碼塊都放在什么位置:
第一份位於try代碼后 : 若果try中代碼正常執行,沒有異常那么finally代碼就在這里執行。
第二份位於catch代碼后 : 如果try中有異常同時被catch捕獲,那么finally代碼就在這里執行。
第三份位於異常執行路徑 : 如果如果try中有異常但沒有被catch捕獲,或者catch又拋異常,那么就執行最終的finally代碼。
問題
:為什么捕獲異常會較大的性能消耗?
因為構造異常的實例比較耗性能
。這從代碼層面很難理解,不過站在JVM的角度來看就簡單了,因為JVM在構造異常實例時需要生成該異常的棧軌跡
。這個操作會逐一訪問當前
線程的棧幀,並且記錄下各種調試信息,包括棧幀所指向方法的名字,方法所在的類名、文件名,以及在代碼中的第幾行觸發該異常等信息。雖然具體不清楚JVM的實現細節,但
是看描述這件事情也是比較費時費力的。
參考
深入拆解 Java 虛擬機(鄭雨迪)
只要自己變優秀了,其他的事情才會跟着好起來(少將7)