天天看點

JVM 異常表及 try-catch-finally 位元組碼分析

雲栖号資訊:【 點選檢視更多行業資訊

在這裡您可以找到不同行業的第一手的上雲資訊,還在等什麼,快來!

作為一個“有經驗”的 Java 工程師,你一定知道什麼是try-catch-finally代碼塊。但是你知道 JVM 是如何處理異常的嗎?今天我們就來講講異常在 JVM 中的處理機制,以及位元組碼中異常表。

希望在這之後,不會有人再将下面這張表情包發給你……

JVM 異常表及 try-catch-finally 位元組碼分析

環境介紹

  • jdk 1.8.0_151
  • IntelliJ IDEA 及 jclasslib 插件

位元組碼中的 try-catch

Talk is cheap, show you my code!

反編譯後的位元組碼

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

JVM 異常表及 try-catch-finally 位元組碼分析

然後在指令行中先定位到這個類的位元組碼檔案目錄中,執行主方法後敲下javap -c 類名進行反編譯,或者直接在編譯器中選擇Build Project,然後打開 jclasslib 工具就可以看到這個類的位元組碼。

我選擇了第二個方法,主方法的位元組碼如下圖:

JVM 異常表及 try-catch-finally 位元組碼分析

可以看到0~3行是 try 代碼塊中的輸出語句,12~17行是 catch 代碼塊中的輸出語句。然後重點來了。

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

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

答案就是——異常表。

異常表

在一個類被編譯成位元組碼之後,它的每個方法中都會有一張異常表。異常表中包含了“監控”的範圍,“監控”何種異常以及抛出異常後去哪裡處理。比如上述的示例代碼,在 jclasslib 中它的異常表如下圖。

JVM 異常表及 try-catch-finally 位元組碼分析

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

JVM 異常表及 try-catch-finally 位元組碼分析

無論是哪種形式的異常表,我們可以知道的是,異常表中每一行就代表一個異常處理器。

  • 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 代碼塊,然後再次執行反編譯的指令看看有什麼不一樣。

JVM 異常表及 try-catch-finally 位元組碼分析
JVM 異常表及 try-catch-finally 位元組碼分析

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 代碼塊之後會重新抛出剛剛捕獲的異常。

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

JVM 異常表及 try-catch-finally 位元組碼分析

可以看到與原來相比異常表增加了兩條,第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,那麼最終傳回的值是什麼?為了說明這個問題,我編寫了一段測試代碼,然後找到它的位元組碼指令。

JVM 異常表及 try-catch-finally 位元組碼分析

正如上文所述,finally 代碼塊會在所有正常及異常的路徑上都複制一份,在這段位元組碼中,iconst_3 就是對應着 finally 代碼塊,共三份,是以即便在 try 或者 catch 代碼塊中有 return 語句,最終還是會會執行 finally 代碼塊中的内容。

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

小結

  • try-catch 語句的位元組碼指令
  • 異常表的介紹及 JVM 中異常處理流程
  • JVM 中關于 finally 代碼塊的特殊處理
  • 關于 return 關鍵字在 try-catch-finally 中的說明

【雲栖号線上課堂】每天都有産品技術專家分享!

課程位址:

https://yqh.aliyun.com/live

立即加入社群,與專家面對面,及時了解課程最新動态!

【雲栖号線上課堂 社群】

https://c.tb.cn/F3.Z8gvnK

原文釋出時間:2020-05-29

本文作者:Planeswalker23

本文來自:“

掘金

”,了解相關資訊可以關注“掘金”