閱讀更多關於 Angular、TypeScript、Node.js/Java 、Spring 等技術文章,歡迎訪問個人我的博客 —— 全棧修仙之路
本文的主要內容分爲 Java 異常的定義、Java 異常的處理、JVM 基礎知識(異常表、JVM 指令分類和操做數棧)及深刻剖析 try-catch-finally 四部分(圖解形式)。在深刻剖析 try-catch-finally 部分會以字節碼的角度分析爲何 finally 語句必定會執行。第三和第四部分理解起來可能會有些難度,不感興趣的小夥伴可直接跳過。html
異常是指在程序執行期間發生的事件,這些事件中斷了正常的指令流(例如,除零,數組越界訪問等)。在 Java 中,異常是一個對象,該對象包裝了方法內發生的錯誤事件,幷包含如下信息:java
此外,異常對象也能夠被拋出或捕獲。Java 程序在執行過程當中發生的異常可分爲兩大類:Error 和 Exception,它們都繼承於 Throwable 類。git
An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch. Most such errors are abnormal conditions.
Error 是 Throwable 類的子類,它表示合理的應用程序不該該嘗試捕獲的嚴重問題。大多數這樣的錯誤都是異常狀況。讓咱們來看一下 Error 類的一些子類,並閱讀 JavaDoc 上與它們有關的註釋:github
這些錯誤是不可查的,由於它們在應用程序的控制和處理能力以外,並且絕大多數是程序運行時不容許出現的情況。面試
The class Exception and its subclasses are a form of Throwable that indicates conditions that a reasonable application might want to catch.
Exception 和它的子類是可拋出異常的一種形式,表示合理的應用程序可能想要捕獲的異常。在 Exception 分支中有一個重要的子類 RuntimeException(運行時異常),該類型的異常會自動爲你所編寫的程序建立ArrayIndexOutOfBoundsException(數組下標越界異常)、NullPointerException(空指針異常)、ArithmeticException(算術異常)、IllegalArgumentException(非法參數異常)等異常,這些異常是不檢查異常,程序中能夠選擇捕獲處理,也能夠不處理。這些異常通常是由程序邏輯錯誤引發的,程序應該從邏輯角度儘量避免這類異常的發生。編程
Error 一般是災難性的致命的錯誤,是程序沒法控制和處理的,當出現這些異常時,Java 虛擬機(JVM)通常會選擇終止線程;Exception 一般狀況下是能夠被程序處理的,而且在程序中應該儘量的去處理這些異常。數組
Unchecked Exception(不受檢查的異常):多是常常出現的編程錯誤,好比 NullPointerException(空指針異常)或 IllegalArgumentException(非法參數異常)。應用程序有時能夠處理它或今後 Throwable 類型的異常中恢復。或者至少在 Thread 的 run 方法中捕獲它,記錄日誌並繼續運行。oracle
Checked Exception(檢查異常):在正確的程序運行過程當中,很容易出現的、情理可容的異常情況,在必定程度上這種異常的發生是能夠預測的,而且一旦發生該種異常,就必須採起某種方式進行處理。app
除了 RuntimeException 及其子類之外,其餘的 Exception 類及其子類都屬於檢查異常,當程序中可能出現這類異常,要麼使用 try-catch 語句進行捕獲,要麼用 throws 子句拋出,不然編譯沒法經過。jvm
不受檢查異常和檢查異常的區別是:不受檢查異常爲編譯器不要求強制處理的異常,檢查異常則是編譯器要求必須處置的異常。
在 Java 中有 5 個關鍵字用於異常處理:try,catch,finally,throws 和 throw(注意 throw 和 throws 之間存在一些區別)。
Java 的異常處理包含三部分:聲明異常、拋出異常和捕獲異常。
一個 Java 方法必須在其簽名中聲明可能經過 throws 關鍵字在其方法體中 「拋出」 的已檢查異常的類型。
舉個例子,假設 methodD()
的定義以下:
public void methodD() throws XxxException, YyyException { // 方法體拋出XxxException和YyyException異常 }
methodD 的方法簽名表示運行 methodD 方法時,可能遇到兩種 checked exceptions:XxxException 和 YyyException。換句話說,在 methodD 方法中若出現某些不正常的狀況可能會觸發 XxxException 或 YyyException 異常。
請注意,咱們不須要聲明屬於 Error,RuntimeException 及其子類的異常。這些異常稱爲不受檢查的異常,由於編譯器未檢查它們。
當 Java 操做遇到異常狀況時,包含錯誤語句的方法應建立一個適當的 Exception 對象,並經過 throw XxxException
語句將其拋到 Java 運行時。例如:
public void methodD() throws XxxException, YyyException { // 方法簽名 // 方法體 ... ... // 出現XxxException異常 if ( ... ) throw new XxxException(...); // 構造一個XxxException對象並拋給JVM ... // 出現YyyException異常 if ( ... ) throw new YyyException(...); // 構造一個YyyException對象並拋給JVM ... }
請注意,在方法簽名中聲明異常的關鍵字爲throws
,在方法體內拋出異常對象的關鍵字爲throw
。
當方法拋出異常時,JVM 在調用堆棧中向後搜索匹配的異常處理程序。每一個異常處理程序均可以處理一類特殊的異常。異常處理程序能夠處理特定的類,也能夠處理其子類。若是在調用堆棧中未找到異常處理程序,則程序終止。
好比,假設 methodD 方法在方法簽名上聲明瞭可能拋出的 XxxException 和 YyyException 異常,具體以下:
public void methodD() throws XxxException, YyyException { ...... }
要在程序中使用 methodD 方法,好比在 methodC 方法中,你能夠這樣作:
try-catch
或 try-catch-finally
中,以下所示。每一個 catch 塊能夠包含一種類型的異常對應的異常處理程序。public void methodC() { // 未聲明異常 ...... try { ...... // 調用聲明XxxException和YyyException異常的methodD方法 methodD(); ...... } catch (XxxException ex) { // 處理XxxException異常 ...... } catch (YyyException ex} { // 處理YyyException異常 ...... } finally { // 可選 // 這些代碼總會執行,用於執行清理操做 ...... } ...... }
public void methodC() throws XxxException, YyyException { // 讓更高層級的方法來處理 ... // 調用聲明XxxException和YyyException異常的methodD方法 methodD(); // 無需使用try-catch ... }
在這種狀況下,若是 methodD 方法拋出 XxxException 或 YyyException,則 JVM 將終止 methodD 方法和methodC 方法並將異常對象沿調用堆棧傳遞給 methodC 方法的調用者。
try-catch-finally 的語法以下:
try { // 主要邏輯,使用了可能拋出異常的方法 ...... } catch (Exception1 ex) { // 處理Exception1異常 ...... } catch (Exception2 ex) { // 處理Exception2異常 ...... } finally { // finally是可選的 // 這些代碼總會執行,用於執行清理操做 ...... }
若是在 try 塊運行期間未發生異常,則將跳過全部 catch 塊,並在 try 塊以後執行 finally 塊。若是 try 塊中的一條語句引起異常,則 Java 運行時將忽略 try 塊中的其他語句,並開始搜索匹配的異常處理程序。它將異常類型與每一個 catch 塊順序匹配。
若是 catch 塊捕獲了該異常類或該異常的超類,則將執行該 catch 塊中的語句。而後,在該catch 塊以後執行 finally 塊中的語句。該程序將在 try-catch-finally 以後繼續進入下一個語句,除非它被過早終止。
若是沒有任何 catch 塊匹配,則異常將沿調用堆棧傳遞。當前方法執行 finally 子句並從調用堆棧中彈出。調用者遵循相同的過程來處理異常。
前面咱們已經介紹了經過使用 try{}catch(){}finally{}
來對異常進行捕獲或者處理。可是對於 JVM 來講,在它內部是如何進行異常處理呢?實際上 Java 編譯後,會在代碼後附加異常表的形式來實現 Java 的異常處理及 finally 機制(JDK 1.4.2 以前,Java 編譯器是使用 jsr 和 ret 指令來實現 finally 語句,JDK1.7 及以後版本,則徹底禁止在 Class 文件中使用 jsr 和 ret 指令)。
屬性表(attribute_info)能夠存在於 Class 文件、字段表、方法表中,用於描述某些場景的專有信息。屬性表中有個 Code 屬性,該屬性在方法表中使用,Java 程序方法體中的代碼被編譯成的字節碼指令存儲在 Code 屬性中。而異常表(exception_table)則是存儲在 Code 屬性表中的一個結構,這個結構是可選的。
異常表結構以下表所示。它包含 4 個字段:若是當字節碼在第 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 |
下面咱們開始來分析一下 一個 catch 語句,多個 catch 語句 和 try-catch-finally 語句 這三種情形所生成的字節碼。從而加深對 JVM 內部 try-catch-finally
機制的理解。
爲了節省篇幅示例代碼就不貼出來了,本人已上傳上傳至 Gist,須要完整代碼的小夥伴請自行獲取。注意:經過 javap -v -p ClassName(編譯後所生成 class 文件的名稱) 能夠查看生成的 class 文件的信息。
由於使用一字節表示操做碼,因此 Java 虛擬機最多隻能支持 256(2^8 )條指令。
Java 虛擬機規範已經定義了 205 條指令,操做碼分別是 0(0x00)到 202(0xCA)、254(0xFE)和 255(0xFF)。這 205 條指令構成了 Java 虛擬機的指令集(instruction set)。
Java 虛擬機規範把已經定義的 205 條指令按用途分紅了 11 類:
保留指令共有 3 條。其中一條是留給調試器的,用於實現斷點,操做碼是202(0xCA)
,助記符是breakpoint
。另外兩條留給Java
虛擬機實現內使用,操做碼分別是254(0xFE)
和266(0xFF)
,助記符是impdep1
和impdep2
。這三條指令不容許出如今 class 文件中。
若想了解完整的 Java 字節碼指令列表,能夠訪問 Wiki - Java_bytecode_instruction_listings 這個頁面。
操做數棧也常稱爲操做棧。它是各類各樣的字節碼操做如何得到他們的輸入,以及他們如何提供他們的輸出。
例如,考慮 iadd 操做,它將兩個 int 添加在一塊兒。要使用它,你在堆棧上推兩個值,而後使用它:
iload_0 # Push the value from local variable 0 onto the stack iload_1 # Push the value from local variable 1 onto the stack iadd # Pops those off the stack, adds them, and pushes the result
如今棧上的頂值是這兩個局部變量的總和。下一個操做可能須要頂層棧值,並將其存儲在某個地方,或者咱們可能在堆棧中推送另外一個值來執行其餘操做。
假設要將三個值添加在一塊兒,堆棧使這很容易:
iload_0 # Push the value from local variable 0 onto the stack iload_1 # Push the value from local variable 1 onto the stack iadd # Pops those off the stack, adds them, and pushes the result iload_2 # Push the value from local variable 2 onto the stack iadd # Pops those off the stack, adds them, and pushes the result
如今棧上的頂值是將這三個局部變量相加在一塊兒的結果。
讓咱們更詳細地看看第二個例子:
咱們假設:
> 堆棧是空的開始
> 局部變量 0 包含 27
> 局部變量 1 包含 10
> 局部變量 2 包含 5
因此最初 stack 的狀態:
+-------+ | stack | +-------+ +-------+
而後咱們執行:
iload_0 # Push the value from local variable 0 onto the stack
當前操做數棧的狀態:
+-------+ | stack | +-------+ | 27 | +-------+
接着繼續執行:
iload_1 # Push the value from local variable 1 onto the stack
當前操做數棧的狀態:
+-------+ | stack | +-------+ | 10 | | 27 | +-------+
如今咱們執行 iadd 指令:
iadd # Pops those off the stack, adds them, and pushes the result
該指令會將 10 和 27 出棧並對它們執行加法運算,完成計算後會把結果繼續入棧。此時操做數棧的狀態爲:
+-------+ | stack | +-------+ | 37 | +-------+
繼續執行如下指令:
iload_2 # Push the value from local variable 2 onto the stack
該指令執行以後,操做數棧的狀態:
+-------+ | stack | +-------+ | 5 | | 37 | +-------+
最後咱們執行 iadd 指令:
iadd # Pops those off the stack, adds them, and pushes the result
該指令執行以後,操做數棧的最終狀態:
+-------+ | stack | +-------+ | 42 | +-------+
前面咱們已經介紹了 Java 中異常和 JVM 虛擬機相關知識,以前恰好看過 字節碼角度看面試題 —— try catch finally 爲啥 finally 語句必定會執行 這篇文章,下面咱們來換個角度,即以 字節碼 的角度來分析一下 try-catch-finally 的底層原理。
注意:如下內容須要對 Java 字節碼有必定的瞭解,請小夥伴們選擇性閱讀。
紅色虛線關聯塊(1)
tryItOut 方法編譯後生成如下代碼:
0: aload_0 1: invokespecial #2
上述代碼的做用是從局部變量表中加載 this,並調用 tryItOut 方法。
藍色虛線關聯塊(2)
catch 語句編譯後生成如下代碼:
7: astore_1 8: aload_0 9: aload_1 10: invokespecial #4
上述代碼的做用是加載 MyException 實例,並調用 handleException 方法。
細心的小夥伴可能會發現生成的 Code 的索引是:0 - 1 - 4 -7 - 8 - 9 - 10 -13,沒有看到 二、3 和 十一、12。我的猜想是由於 JVM 字節碼指令 invokespecial 操做數佔用了 2 個索引字節(歡迎知道真相的大佬,慷慨解答)。這裏 invokespecial 字節碼指令的格式定義以下:
invokespecial indexbyte1 indexbyte2
Exception table
當字節碼在第 0 行到 4 行之間(包括 0 行而不包括 4 行)出現了類型爲 MyException 類型或者其子類的異常,則跳轉到第 7 行。若 type 的值爲 0 時,表示任意異常狀況都須要轉向到 target 處進行處理。
從上圖可知,若存在多個 catch 語句,則異常表中會生成多條記錄。astore_1 字節碼指令的做用是把引用(異常對象 e)存入局部變量表。
基於上圖咱們來詳細分析一下生成的字節碼:
根據上述的分析和圖中三個虛線框標出的字節碼,相信你們已經知道在 Java 的 try-catch-finally 語句中 finally 語句必定會執行的最終緣由了。