(一)JVM 如何處理異常信息?

先上一張圖:

在Java 規範中,所有的異常被定義爲 Throwable 或其子類的實例。其中 Error 表示該線程執行狀態無法恢復,需要終止線程甚至是虛擬機;Exception 可以被捕捉並處理。

 

異常的設計初衷是將運行時產生的錯誤信息通過某種方式傳遞給某個接收者——該接收者將知道如何正確的處理這個問題。Java 使用異常來提供一致的錯誤報告模型,使得構件能夠與客戶端代碼可靠地溝通問題。實際上,異常處理的一個重要目標就是把錯誤處理的代碼同錯誤發生的地點分離,使得你在某處專注於要完成的事,而在另一處處理錯誤。既分離了主幹代碼和錯誤處理邏輯,又可以重用錯誤處理代碼。——以上摘自《Thinking in Java》

Java 異常分爲兩大類: 檢查異常( Checked Exception ) 和非檢查異常 ( Unchecked Exception ) 。在Java 語法中,所有的檢查異常都需要顯示的捕獲或者在方法的聲明中用throws 關鍵字標註 (不然爲啥叫檢查異常呢),目的是最大化的利用編譯器的編譯時檢查,寫出魯棒性高的程序。異常處理(拋出異常和捕捉異常)實現了程序控制流的非正常轉移。

 

拋出異常又分爲顯式拋出和隱式拋出。顯式拋出的主體是應用程序,用代碼體現出來就是用 throws 手動拋出異常;隱式拋出的主體則是虛擬機,在其執行的過程中碰到了無法繼續執行的異常狀態。

 

捕獲異常則涉及瞭如下三種代碼塊:

try 代碼塊:用來標記需要進行異常監控的代碼。

catch 代碼塊:跟在 try 代碼塊之後,用來捕獲在 try 代碼塊中觸發的某種指定類型的異常。在 Java 中,try 代碼塊後面可以跟着多個 catch 代碼塊,來捕獲不同類型的異常,Java 虛擬機會從上至下匹配異常處理器。因此,前面的 catch 代碼塊所捕獲的異常類型不能覆蓋後邊的,否則編譯器會報錯。

finally 代碼塊:跟在 try 代碼塊和 catch 代碼塊之後,用來聲明一段必定運行的代碼。它的設計初衷是爲了避免跳過某些關鍵的清理代碼,例如關閉已打開的系統資源。

JVM 是如何保證 finally 代碼塊一定會執行?

public class T {

    private int a,b,c,d;

    public void  test(){

        try{
            a=1;
        }catch (Exception e){
            b=2;
        }finally {
            c=3;
        }
        d=4;
    }

}

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

當程序觸發異常時,Java 虛擬機會從上至下遍歷( 前面的 catch 代碼塊所捕獲的異常類型不能覆蓋後邊 的原因就在此 異常表中的所有條目。當觸發異常的字節碼的索引值在某個異常表條目的監控範圍內,Java 虛擬機會判斷所拋出的異常和該條目想要捕獲的異常是否匹配。如果匹配,Java 虛擬機會將控制流轉移至該條目 target 指針指向的字節碼。

如果遍歷完所有異常表條目,Java 虛擬機仍未匹配到異常處理器,那麼它會彈出當前方法對應的 Java 棧幀,並且在調用者(caller)中重複上述操作。在最壞情況下,Java 虛擬機需要遍歷當前線程 Java 棧上所有方法的異常表。

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

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

查看編譯後的代碼:

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

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

P.S. 本系列文章爲學習出自鄭雨迪的《深入拆解 Java 虛擬機》課程整理筆記。購買請掃描下方二維碼:

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章