JVM 異常表及 try-catch-finally 字節碼分析

做爲一個「有經驗」的 Java 工程師,你必定知道什麼是try-catch-finally代碼塊。可是你知道 JVM 是如何處理異常的嗎?今天咱們就來說講異常在 JVM 中的處理機制,以及字節碼中異常表。java

但願在這以後,不會有人再將下面這張表情包發給你……bash

環境介紹

  • jdk 1.8.0_151
  • IntelliJ IDEA 及 jclasslib 插件

字節碼中的 try-catch

Talk is cheap, show you my code!工具

反編譯後的字節碼

首先我編寫了第一段測試代碼,這裏有一個 try-catch 代碼塊,每一個代碼塊中都有一行輸出,在 catch 代碼塊中捕獲的是 Exception 異常。測試

public static void main(String[] args) {
        try {
            System.out.println("enter try block");
        } catch (Exception e) {
            System.out.println("enter catch block");
        }
    }
複製代碼

而後在命令行中先定位到這個類的字節碼文件目錄中,執行主方法後敲下javap -c 類名進行反編譯,或者直接在編譯器中選擇Build Project,而後打開 jclasslib 工具就能夠看到這個類的字節碼。ui

我選擇了第二個方法,主方法的字節碼以下圖:spa

258D7340-1258-4DFE-A8FF-4D97CE07AB95.png

能夠看到0~3行是 try 代碼塊中的輸出語句,12~17行是 catch 代碼塊中的輸出語句。而後重點來了。插件

timg.jpeg

第8行的字節碼是8 goto 20,這是什麼意思呢?沒錯,盲猜就能猜到,這個字節碼指令就是跳轉到第20行的意思。這一行是說,若是 try 代碼塊中沒有出現異常,那麼就跳轉到第20行,也就是整個方法行完成後 return 了。命令行

這是正常的代碼執行流程,那麼若是出現異常了,虛擬機是如何知道應該「監控」 try 代碼塊?它又是怎麼知道該捕獲何種異常呢?code

答案就是——異常表。cdn

異常表

在一個類被編譯成字節碼以後,它的每一個方法中都會有一張異常表。異常表中包含了「監控」的範圍,「監控」何種異常以及拋出異常後去哪裏處理。好比上述的示例代碼,在 jclasslib 中它的異常表以下圖。

81B9AED6-8E67-4A5E-853D-ACD295D7D504.png

或者在javap -c命令下異常表是這樣的:

Exception table:
   from    to  target type
       0     8    11   Class java/lang/Exception
複製代碼

不管是哪一種形式的異常表,咱們能夠知道的是,異常表中每一行就表明一個異常處理器。

  • Nr. 表明異常處理器的序號
  • Start PC (from),表明異常處理器所監控範圍的起始點
  • End PC (to),表明異常處理器所監控範圍的結束點(該行不被包括在監控範圍內,通常是 goto 指令)
  • Handler PC (target),指向異常處理器的起始位置,在這裏就是 catch 代碼塊的起始位置
  • Catch Type (type),表明異常處理器所捕獲的異常類型

若是程序觸發了異常,Java 虛擬機會按照序號遍歷異常表,當觸發的異常在這條異常處理器的監控範圍內(from 和 to),且異常類型(type)與該異常處理器一致時,Java 虛擬機就會跳轉到該異常處理器的起始位置(target)開始執行字節碼。

若是程序沒有觸發異常,那麼虛擬機會使用 goto 指令跳過 catch 代碼塊,執行 finally 語句或者方法返回。

字節碼中的 finally

接下來在上述的代碼中再加入一個 finally 代碼塊,而後再次執行反編譯的命令看看有什麼不同。

// 源代碼
public static void main(String[] args) {
        try {
            // dosomething
            System.out.println("enter try block");
        } catch (Exception e) {
            System.out.println("enter catch block");
        } finally {
            System.out.println("enter finally block");
        }
    }
複製代碼
// 字節碼
 0 getstatic #2 <java/lang/System.out>
 3 ldc #3 <enter try block>
 5 invokevirtual #4 <java/io/PrintStream.println>
 8 getstatic #2 <java/lang/System.out>
11 ldc #5 <enter finally block>
13 invokevirtual #4 <java/io/PrintStream.println>
16 goto 50 (+34)
19 astore_1
20 getstatic #2 <java/lang/System.out>
23 ldc #7 <enter catch block>
25 invokevirtual #4 <java/io/PrintStream.println>
28 getstatic #2 <java/lang/System.out>
31 ldc #5 <enter finally block>
33 invokevirtual #4 <java/io/PrintStream.println>
36 goto 50 (+14)
39 astore_2
40 getstatic #2 <java/lang/System.out>
43 ldc #5 <enter finally block>
45 invokevirtual #4 <java/io/PrintStream.println>
48 aload_2
49 athrow
50 return
複製代碼

finally 代碼塊在當前版本(jdk 1.8)的 JVM 中的處理機制是比較特殊的。從上面的字節碼中也能夠明顯看到,只是加了一個 finally 代碼塊而已,字節碼指令增長了不少行。

若是再仔細觀察一下,咱們能夠發現,在字節碼指令中,有三塊重複的字節碼指令,分別是8~13行、28~33行和40~45行,若是對字節碼有些瞭解的同窗或許已經知道了,這三塊重複的字節碼就是 finally 代碼塊對應的代碼。

出現三塊重複字節碼指令的緣由是在 JVM 中,全部異常路徑(如try、catch)以及全部正常執行路徑的出口都會被附加一份 finally 代碼塊。也就是說,在上述的示例代碼中,try 代碼塊後面會跟着一份 finally 的代碼,catch 代碼塊後面也是如此,再加上本來正常流程會執行的 finally 代碼塊,在字節碼中一共有三份 finally 代碼塊代碼塊。

而針對每一條可能出現的異常的路徑,JVM 都會在異常表中多生成一條異常處理器,用來監控整個 try-catch 代碼塊,同時它會捕獲全部種類的異常,而且在執行完 finally 代碼塊以後會從新拋出剛剛捕獲的異常。

上述示例代碼的異常表以下

Exception table:
   from    to  target type
       0     8    19   Class java/lang/Exception
       0     8    39   any
      19    28    39   any
複製代碼

能夠看到與原來相比異常表增長了兩條,第2條異常處理器異常監控 try 代碼塊,第3條異常處理器監控 catch 代碼塊,若是出現異常則會跳轉到第39行的 finally 代碼塊執行。

這就是 finally 必定會在 try-catch 代碼塊以後執行的緣由了(某些能中斷程序運行的操做除外)。

若是 finally 也拋出異常

上文說到虛擬機會對整個 try-catch 代碼塊生成一個或多個異常處理器,若是在 catch 代碼塊中拋出了異常,這個異常會被捕獲,而且在執行完 finally 代碼塊以後被從新拋出。

那麼在這裏有一個額外的問題須要說起,假設在 catch 代碼塊中拋出了異常 A,當執行 finally 代碼塊時又拋出了異常 B,那麼最後拋出的是什麼異常呢?

若是有同窗本身嘗試過這個操做,就會知道最後拋出的異常 B。也就是說,在捕獲了 catch 代碼塊中的異常後,若是 finally 代碼塊中也拋出了異常,那麼最終將會拋出 finally 中拋出的異常,而原來 catch 代碼塊中的異常將會被忽略。

若是代碼塊中有 return

講完了異常在各個代碼塊中的狀況,接下來再來考慮一下 return 關鍵字吧,若是 try 或者 catch 中有 return,finally 還會執行嗎?若是 finally 中也有 return,那麼最終返回的值是什麼?爲了說明這個問題,我編寫了一段測試代碼,而後找到它的字節碼指令。

public static int get() {
    try {
        return 1;
    } catch (Exception e) {
        return 2;
    } finally {
        return 3;
    }
}

// 字節碼指令
 0 iconst_1
 1 istore_0
 2 iconst_3
 3 ireturn
 4 astore_0
 5 iconst_2
 6 istore_1
 7 iconst_3
 8 ireturn
 9 astore_2
10 iconst_3
11 ireturn
複製代碼

正如上文所述,finally 代碼塊會在全部正常及異常的路徑上都複製一份,在這段字節碼中,iconst_3 就是對應着 finally 代碼塊,共三份,因此即使在 try 或者 catch 代碼塊中有 return 語句,最終仍是會會執行 finally 代碼塊中的內容。

也就是說,這個方法最終的返回結果是3。

小結

  1. try-catch 語句的字節碼指令
  2. 異常表的介紹及 JVM 中異常處理流程
  3. JVM 中關於 finally 代碼塊的特殊處理
  4. 關於 return 關鍵字在 try-catch-finally 中的說明
相關文章
相關標籤/搜索