最近寫程式碼的時候遇到一些try catch的問題。
try {
程式碼塊1
} catch (Exception e) {
程式碼塊2
} finally {
程式碼塊3
}
複製程式碼
在程式碼塊1執行的時候發生異常,但是程式碼塊2沒有執行,程式碼塊3執行了,排查半天發現程式碼塊1中丟擲的並不是Exception及其子類。那麼沒有catch住的try catch流程到底是怎麼樣的呢?
之前也簡單看過一些jvm try catch原理,這裡嘗試記錄總結一下。
Java 在程式碼中通過使用 try{}catch(){}finally{}
塊來對異常進行捕獲或者處理。但是對於 JVM 來說,是如何處理 try/catch 程式碼塊與異常的呢。
實際上 Java 編譯後,會在程式碼後附加異常表的形式來實現 Java 的異常處理及 finally 機制(在 JDK1.4.2之前,javac 編譯器使用 jsr 和 ret 指令來實現 finally 語句,但是1.4.2之後自動在每段可能的分支路徑後將 finally 語句塊內容冗餘生成一遍來實現。JDK1.7及之後版本,則完全禁止在 Class 檔案中使用 jsr 和 ret 指令)。
異常表
屬性表(attribute_info)可以存在於 Class 檔案、欄位表、方法表中,用於描述某些場景的專有資訊。屬性表中有個 Code 屬性,該屬性在方法表中使用,Java 程式方法體中的程式碼被編譯成的位元組碼指令儲存在 Code 屬性中。而異常表(exception_table)則是儲存在 Code 屬性表中的一個結構,這個結構是可選的。
異常表結構
異常表結構如下表所示。它包含四個欄位:如果當位元組碼在第 start_pc 行到 end_pc 行之間(即[start_pc, end_pc))出現了型別為 catch_type 或者其子類的異常(catch_type 為指向一個 CONSTANT_Class_info 型常量的索引),則跳轉到第 handler_pc 行執行。如果 catch_type 為0,表示任意異常情況都需要轉到 handler_pc 處進行處理。
型別 | 名稱 | 數量 |
---|---|---|
u2 | start_pc | 1 |
u2 | end_pc | 1 |
u2 | handler_pc | 1 |
u2 | catch_type | 1 |
處理異常機制
如上面所說,每個類編譯後,都會跟隨一個異常表,如果發生異常,首先在異常表中查詢對應的行(即程式碼中相應的 try{}catch(){}
程式碼塊),如果找到,則跳轉到異常處理程式碼執行,如果沒有找到,則返回(執行 finally 之後),並 copy 異常的應用給父呼叫者,接著查詢父呼叫的異常表,以此類推。
異常處理例項
對於 Java 原始碼:
public class Test {
public int inc() {
int x;
try {
x = 1;
return x;
} catch (Exception e) {
x = 2;
return x;
} finally {
x = 3;
}
}
}
複製程式碼
將其編譯為 ByteCode 位元組碼(JDK版本1.8):
public int inc();
Code:
0: iconst_1 #try中x=1入棧
1: istore_1 #x=1存入第二個int變數
2: iload_1 #將第二個int變數推到棧頂
3: istore_2 #將棧頂元素存入第三個變數,即儲存try中的返回值
4: iconst_3 #final中的x=3入棧
5: istore_1 #棧頂元素放入第二個int變數,即final中的x=3
6: iload_2 #將第三個int變數推到棧頂,即try中的返回值
7: ireturn #當前方法返回int,即x=1
8: astore_2 #棧頂數值放入當前frame的區域性變數陣列中第三個
9: iconst_2 #catch中的x=2入棧
10: istore_1 #x=2放入第二個int變數
11: iload_1 #將第二個int變數推到棧頂
12: istore_3 #將棧頂元素存入第四個變數,即儲存catch中的返回值
13: iconst_3 #final中的x=3入棧
14: istore_1 #final中的x=3放入第一個int變數
15: iload_3 #將第四個int變數推到棧頂,即儲存的catch中的返回值
16: ireturn #當前方法返回int,即x=2
17: astore 4 #棧頂數值放入當前frame的區域性變數陣列中第五個
19: iconst_3 #final中的x=3入棧
20: istore_1 #final中的x=3放入第一個int變數
21: aload 4 #當前frame的區域性變數陣列中第五個放入棧頂
23: athrow #將棧頂的數值作為異常或錯誤丟擲
Exception table:
from to target type
0 4 8 Class java/lang/Exception
0 4 17 any
8 13 17 any
17 19 17 any
複製程式碼
首先可以看到,對於 finally,編譯器將每個可能出現的分支後都放置了冗餘。並且編譯器生成了三個異常表記錄,從 Java 程式碼的語義上講,執行路徑分別為:
- 如果 try 語句塊中出現了屬於 Exception 及其子類的異常,則跳轉到 catch 處理;
- 如果 try 語句塊中出現了不屬於 Exception 及其子類的異常,則跳轉到 finally 處理;
- 如果 catch 語句塊中出現了任何異常,則跳轉到 finally 處理。
由此可以分析此段程式碼可能的返回結果:
- 如果沒有出現異常,返回1;
- 如果出現 Exception 異常,返回2;
- 如果出現了 Exception 意外的異常,非正常退出,沒有返回;
我們來分析位元組碼:
首先,0-3行,就是把整數1賦值給 x,並且將此時 x 的值複製一個副本到本地變數表的 Slot 中暫存,這個 Slot 裡面的值在 ireturn 指令執行前會被重新讀到棧頂,作為返回值。這時如果沒有異常,則執行4-5行,把 x 賦值為3,然後返回前面儲存的1,方法結束。如果出現異常,讀取異常表發現應該執行第8行,pc 暫存器指標轉向8行,8-16行就是把2賦值給 x,然後把 x 暫存起來,再將 x 賦值為3,然後將暫存的2讀到操作棧頂返回。第17行開始是把 x 賦值為3並且將棧頂的異常丟擲,方法結束。
上面是一個比較簡單的Java程式,這裡稍微複雜化它,嘗試在finally中增加異常模組:
public class Test {
public int inc() {
int x;
try {
x = 1;
return x;
} catch (Exception e) {
x = 2;
return x;
} finally {
try{
x = 3;
} catch (Exception e) {
x = 4;
}
}
}
}
複製程式碼
將其編譯為 ByteCode 位元組碼:
public int inc();
Code:
0: iconst_1
1: istore_1
2: iload_1
3: istore_2
4: iconst_3
5: istore_1
6: goto 12
9: astore_3
10: iconst_4
11: istore_1
12: iload_2
13: ireturn
14: astore_2
15: iconst_2
16: istore_1
17: iload_1
18: istore_3
19: iconst_3
20: istore_1
21: goto 28
24: astore 4
26: iconst_4
27: istore_1
28: iload_3
29: ireturn
30: astore 5
32: iconst_3
33: istore_1
34: goto 41
37: astore 6
39: iconst_4
40: istore_1
41: aload 5
43: athrow
Exception table:
from to target type
4 6 9 Class java/lang/Exception
0 4 14 Class java/lang/Exception
19 21 24 Class java/lang/Exception
0 4 30 any
14 19 30 any
32 34 37 Class java/lang/Exception
30 32 30 any
複製程式碼
和上面一樣,0-3行為try內語句,儲存x=1並準備返回,如果發生異常則查詢異常表,跳轉執行14行;14-18行為catch部分語句,儲存x=2並準備返回;4-6行、19-21行、32-34行為finally中語句,首先設定x=3,如果沒有發生異常,則之後進行跳轉,否則往下執行,即執行astore
,iconst
,istore
,即保留之前的棧頂位置,對x賦值為4。
最後總結一下,Java通過異常表來捕捉異常,在表中針對發生的異常能夠獲取接下來執行到哪裡(從try跳轉到catch),除了指定的異常外,還會自動追加any異常,用來捕獲程式中沒有捕獲的異常。而finally會自動的追加到try、catch以及未捕獲到的異常後面執行。對於多層次的try{}catch{}
,同理。
ps. 最後有一個彩蛋,就是異常表後面會追加一個指向自己start_pc的條目,這裡有一些討論可以看看。
碼字不易,如有建議請掃碼