Java 異常表與異常處理原理
最近寫程式碼的時候遇到一些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: astore4#棧頂數值放入當前frame的區域性變數陣列中第五個 19: iconst_3#final中的x=3入棧 20: istore_1#final中的x=3放入第一個int變數 21: aload4#當前frame的區域性變數陣列中第五個放入棧頂 23: athrow#將棧頂的數值作為異常或錯誤丟擲 Exception table: fromtotarget type 048Class java/lang/Exception 0417any 81317any 171917any 複製程式碼
首先可以看到,對於 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: goto12 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: goto28 24: astore4 26: iconst_4 27: istore_1 28: iload_3 29: ireturn 30: astore5 32: iconst_3 33: istore_1 34: goto41 37: astore6 39: iconst_4 40: istore_1 41: aload5 43: athrow Exception table: fromtotarget type 469Class java/lang/Exception 0414Class java/lang/Exception 192124Class java/lang/Exception 0430any 141930any 323437Class java/lang/Exception 303230any 複製程式碼
和上面一樣,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的條目,這裡有一些討論可以看看。