還搞不清楚JVM是怎麼處理異常的?這水平,跳槽都沒人要

本文已收錄GitHub,更有互聯網大廠面試真題,面試攻略,高效學習資料等java

衆所周知,異常處理的兩大組成要素是拋出異常和捕獲異常。這兩大要素共同實現程序控制流的非正常轉移。git

拋出異常可分爲顯式和隱式兩種。顯式拋異常的主體是應用程序,它指的是在程序中使用「throw」關鍵字,手動將異常實例拋出。github

隱式拋異常的主體則是 Java 虛擬機,它指的是 Java 虛擬機在執行過程當中,碰到沒法繼續執行的異常狀態,自動拋出異常。舉例來講,Java 虛擬機在執行讀取數組操做時,發現輸入的索引值是負數,故而拋出數組索引越界異常(ArrayIndexOutOfBoundsException)。面試

捕獲異常則涉及了以下三種代碼塊數組

  1. try 代碼塊:用來標記須要進行異常監控的代碼。
  2. catch 代碼塊:跟在 try 代碼塊以後,用來捕獲在 try 代碼塊中觸發的某種指定類型的異常。除了聲明所捕獲異常的類型以外,catch 代碼塊還定義了針對該異常類型的異常處理器。在 Java 中,try 代碼塊後面能夠跟着多個 catch 代碼塊,來捕獲不一樣類型的異常。Java 虛擬機會從上至下匹配異常處理器。所以,前面的 catch 代碼塊所捕獲的異常類型不能覆蓋後邊的,不然編譯器會報錯。
  3. finally 代碼塊:跟在 try 代碼塊和 catch 代碼塊以後,用來聲明一段一定運行的代碼。它的設計初衷是爲了不跳過某些關鍵的清理代碼,例如關閉已打開的系統資源。

在程序正常執行的狀況下,這段代碼會在 try 代碼塊以後運行。不然,也就是 try 代碼塊觸發異常的狀況下,若是該異常沒有被捕獲,finally 代碼塊會直接運行,而且在運行以後從新拋出該異常。緩存

若是該異常被 catch 代碼塊捕獲,finally 代碼塊則在 catch 代碼塊以後運行。在某些不幸的狀況下,catch 代碼塊也觸發了異常,那麼 finally 代碼塊一樣會運行,並會拋出 catch代碼塊觸發的異常。在某些極端不幸的狀況下,finally 代碼塊也觸發了異常,那麼只好中斷當前 finally 代碼塊的執行,並往外拋異常。ide

上面這段聽起來有點繞,可是等我講完 Java 虛擬機的異常處理機制以後,你便會明白這其中的道理。工具

異常的基本概念

在 Java 語言規範中,全部異常都是 Throwable 類或者其子類的實例。Throwable 有兩大直接子類。第一個是 Error,涵蓋程序不該捕獲的異常。當程序觸發 Error 時,它的執行狀態已經沒法恢復,須要停止線程甚至是停止虛擬機。第二子類則是 Exception,涵蓋程序可能須要捕獲而且處理的異常。學習

還搞不清楚JVM是怎麼處理異常的?這水平,跳槽都沒人要

Exception 有一個特殊的子類 RuntimeException,用來表示「程序雖然沒法繼續執行,可是還能搶救一下」的狀況。前邊提到的數組索引越界即是其中的一種。this

RuntimeException 和 Error 屬於 Java 裏的非檢查異常(unchecked exception)。其餘異常則屬於檢查異常(checked exception)。在 Java 語法中,全部的檢查異常都須要程序顯式地捕獲,或者在方法聲明中用 throws 關鍵字標註。一般狀況下,程序中自定義的異常應爲檢查異常,以便最大化利用 Java 編譯器的編譯時檢查。

異常實例的構造十分昂貴。這是因爲在構造異常實例時,Java 虛擬機便須要生成該異常的棧軌跡(stack trace)。該操做會逐一訪問當前線程的 Java 棧幀,而且記錄下各類調試信息,包括棧幀所指向方法的名字,方法所在的類名、文件名,以及在代碼中的第幾行觸發該異常。

固然,在生成棧軌跡時,Java 虛擬機會忽略掉異常構造器以及填充棧幀的 Java 方法(Throwable.fillInStackTrace),直接重新建異常位置開始算起。此外,Java 虛擬機還會忽略標記爲不可見的 Java 方法棧幀。咱們在介紹 Lambda 的時候會看到具體的例子。

既然異常實例的構造十分昂貴,咱們是否能夠緩存異常實例,在須要用到的時候直接拋出呢?從語法角度上來看,這是容許的。然而,該異常對應的棧軌跡並不是 throw 語句的位置,而是新建異常的位置。

所以,這種作法可能會誤導開發人員,使其定位到錯誤的位置。這也是爲何在實踐中,咱們每每選擇拋出新建異常實例的緣由。

Java虛擬機是如何捕獲異常的?

在編譯生成的字節碼中,每一個方法都附帶一個異常表。異常表中的每個條目表明一個異常處理器,而且由 from 指針、to 指針、target 指針以及所捕獲的異常類型構成。這些指針的值是字節碼索引(bytecode index,bci),用以定位字節碼。

其中,from 指針和 to 指針標示了該異常處理器所監控的範圍,例如 try 代碼塊所覆蓋的範圍。target 指針則指向異常處理器的起始位置,例如 catch 代碼塊的起始位置。

public static void main(String[] args) {
    try {
        mayThrowException();
    }
    catch (Exception e) {
        e.printStackTrace();
    }
}
//對應的Java字節碼
public static void main(java.lang.String[]);
Code:
0: invokestatic mayThrowException:()V
3: goto 11
6: astore_1
7: aload_1
8: invokevirtual java.lang.Exception.printStackTrace11: return
Exception table:
from  to target type
0   3   6  Class java/lang/Exception  //異常表條目

舉個例子,在上圖的 main 方法中,我定義了一段 try-catch 代碼。其中,catch 代碼塊所捕獲的異常類型爲 Exception。

編譯事後,該方法的異常表擁有一個條目。其 from 指針和 to 指針分別爲 0 和 3,表明它的監控範圍從索引爲 0 的字節碼開始,到索引爲 3 的字節碼結束(不包括 3)。該條目的target 指針是 6,表明這個異常處理器從索引爲 6 的字節碼開始。條目的最後一列,表明該異常處理器所捕獲的異常類型正是 Exception。

當程序觸發異常時,Java 虛擬機會從上至下遍歷異常表中的全部條目。當觸發異常的字節碼的索引值在某個異常表條目的監控範圍內,Java 虛擬機會判斷所拋出的異常和該條目想要捕獲的異常是否匹配。若是匹配,Java 虛擬機會將控制流轉移至該條目 target 指針指向的字節碼。

若是遍歷完全部異常表條目,Java 虛擬機仍未匹配到異常處理器,那麼它會彈出當前方法對應的 Java 棧幀,而且在調用者(caller)中重複上述操做。在最壞狀況下,Java 虛擬機須要遍歷當前線程 Java 棧上全部方法的異常表。

finally 代碼塊的編譯比較複雜。當前版本 Java 編譯器的作法,是複製 finally 代碼塊的內容,分別放在 try-catch 代碼塊全部正常執行路徑以及異常執行路徑的出口中。

還搞不清楚JVM是怎麼處理異常的?這水平,跳槽都沒人要

針對異常執行路徑,Java 編譯器會生成一個或多個異常表條目,監控整個 try-catch 代碼塊,而且捕獲全部種類的異常(在 javap 中以 any 指代)。這些異常表條目的 target 指針將指向另外一份複製的 finally 代碼塊。而且,在這個 finally 代碼塊的最後,Java 編譯器會從新拋出所捕獲的異常。

若是你感興趣的話,能夠用 javap 工具來查看下面這段包含了 try-catch-finally 代碼塊的編譯結果。爲了更好地區分每一個代碼塊,我定義了四個實例字段:tryBlock、catchBlock、finallyBlock、以及 methodExit,而且僅在對應的代碼塊中訪問這些字段。

public class Foo {
    private int tryBlock;
    private int catchBlock;
    private int finallyBlock;
    private int methodExit;
    public void test() {
        try {
            tryBlock = 0;
        }
        catch (Exception e) {
            catchBlock = 1;
        }
        finally {
            finallyBlock = 2;
        }
        methodExit = 3;
    }
}
$ javap -c Foo
...
public void test();
Code:
0: aload_0
1: iconst_0
2: putfield      #20                 // Field tryBlock:I
5: goto          30
8: astore_1
9: aload_0
10: iconst_1
11: putfield      #22                 // Field catchBlock:I
14: aload_0
15: iconst_2
16: putfield      #24                 // Field finallyBlock:I
19: goto          35
22: astore_2
23: aload_0
24: iconst_2
25: putfield      #24                 // Field finallyBlock:I
28: aload_2
29: athrow
30: aload_0
31: iconst_2
32: putfield      #24                 // Field finallyBlock:I
35: aload_0
36: iconst_3
37: putfield      #26                 // Field methodExit:I40: return
Exception table:
from    to  target type
0     5     8   Class java/lang/Exception
0    14    22   any
...

能夠看到,編譯結果包含三份 finally 代碼塊。其中,前兩份分別位於 try 代碼塊和 catch代碼塊的正常執行路徑出口。最後一份則做爲異常處理器,監控 try 代碼塊以及 catch 代碼塊。它將捕獲 try 代碼塊觸發的、未被 catch 代碼塊捕獲的異常,以及 catch 代碼塊觸發的異常。

這裏有一個小問題,若是 catch 代碼塊捕獲了異常,而且觸發了另外一個異常,那麼 finally捕獲而且重拋的異常是哪一個呢?答案是後者。也就是說本來的異常便會被忽略掉,這對於代碼調試來講十分不利。

Java 7的Supressed異常以及語法糖

Java 7 引入了 Supressed 異常來解決這個問題。這個新特性容許開發人員將一個異常附於另外一個異常之上。所以,拋出的異常能夠附帶多個異常的信息。

然而,Java 層面的 finally 代碼塊缺乏指向所捕獲異常的引用,因此這個新特性使用起來很是繁瑣。

爲此,Java 7 專門構造了一個名爲 try-with-resources 的語法糖,在字節碼層面自動使用Supressed 異常。固然,該語法糖的主要目的並非使用 Supressed 異常,而是精簡資源打開關閉的用法。

在 Java 7 以前,對於打開的資源,咱們須要定義一個 finally 代碼塊,來確保該資源在正常或者異常執行情況下都能關閉。

資源的關閉操做自己容易觸發異常。所以,若是同時打開多個資源,那麼每個資源都要對應一個獨立的 try-finally 代碼塊,以保證每一個資源都可以關閉。這樣一來,代碼將會變得十分繁瑣。

FileInputStream in0 = null;
FileInputStream in1 = null;
FileInputStream in2 = null;
...
try {
    in0 = new FileInputStream(new File("in0.txt"));
    ...
    try {
        in1 = new FileInputStream(new File("in1.txt"));
        ...
        try {
            in2 = new FileInputStream(new File("in2.txt"));
            ...
        }
        finally {
            if (in2 != null) in2.close();
        }
    }
    finally {
        if (in1 != null) in1.close();
    }
}
finally {
    if (in0 != null) in0.close();
}

Java 7 的 try-with-resources 語法糖,極大地簡化了上述代碼。程序能夠在 try 關鍵字後聲明並實例化實現了 AutoCloseable 接口的類,編譯器將自動添加對應的 close() 操做。在聲明多個 AutoCloseable 實例的狀況下,編譯生成的字節碼相似於上面手工編寫代碼的編譯結果。與手工代碼相比,try-with-resources 還會使用 Supressed 異常的功能,來避免原異常「被消失」。

public class Foo implements AutoCloseable {
    private final String name;
    public Foo(String name) {
        this.name = name;
    }
    @Override
    public void close() {
        throw new RuntimeException(name);
    }
    public static void main(String[] args) {
        try (Foo foo0 = new Foo("Foo0");
        // try-with-resources
        Foo foo1 = new Foo("Foo1");
        Foo foo2 = new Foo("Foo2")) {
            throw new RuntimeException("Initial");
        }
    }
}
//運行結果:
Exception in thread "main" java.lang.RuntimeException: Initial
at Foo.main(Foo.java:18)
Suppressed: java.lang.RuntimeException: Foo2
at Foo.close(Foo.java:13)
at Foo.main(Foo.java:19)
Suppressed: java.lang.RuntimeException: Foo1
at Foo.close(Foo.java:13)
at Foo.main(Foo.java:19)
Suppressed: java.lang.RuntimeException: Foo0
at Foo.close(Foo.java:13)
at Foo.main(Foo.java:19)

除了 try-with-resources 語法糖以外,Java 7 還支持在同一 catch 代碼塊中捕獲多種異常。實際實現很是簡單,生成多個異常表條目便可。

//在同一catch代碼塊中捕獲多種異常
try {
    ...
}
catch (SomeException | OtherException e) {
    ...
}

總結與實踐

本文介紹了 Java 虛擬機的異常處理機制。

Java 的異常分爲 Exception 和 Error 兩種,而 Exception 又分爲 RuntimeException 和其餘類型。RuntimeException 和 Error 屬於非檢查異常。其餘的 Exception 皆屬於檢查異常,在觸發時須要顯式捕獲,或者在方法頭用 throws 關鍵字聲明。

Java 字節碼中,每一個方法對應一個異常表。當程序觸發異常時,Java 虛擬機將查找異常表,並依此決定須要將控制流轉移至哪一個異常處理器之中。Java 代碼中的 catch 代碼塊和finally 代碼塊都會生成異常表條目。

Java 7 引入了 Supressed 異常、try-with-resources,以及多異常捕獲。後二者屬於語法糖,可以極大地精簡咱們的代碼。

那麼本文的實踐環節,你能夠看看其餘控制流語句與 finally 代碼塊之間的協做。

//編譯並用javap -c查看編譯後的字節碼
public class Foo {
    private int tryBlock;
    private int catchBlock;
    private int finallyBlock;
    private int methodExit;
    public void test() {
        for (int i = 0; i < 100; i++) {
            try {
                tryBlock = 0;
                if (i < 50) {
                    continue;
                } else if (i < 80) {
                    break;
                } else {
                    return;
                }
            }
            catch (Exception e) {
                catchBlock = 1;
            }
            finally {
                finallyBlock = 2;
            }
        }
        methodExit = 3;
    }
}
相關文章
相關標籤/搜索