在 JAVA 中,異常處理的方式主要是拋出異常和捕獲異常。這兩大要素共同實現程序控制流的非正常轉移。html
拋出異常能夠分爲顯示和隱式兩種。顯示拋出異常的主體是應用程序,它指的是在程序中使用 throw 關鍵字,手動將異常實例拋出。隱式拋出異常的主題是 Java 虛擬機,它指的是 Java 虛擬機在執行過程當中,碰到沒法繼續執行的異常狀態,自動拋出異常。例如數組越界異常。數組
捕獲異常主要設計一下三種代碼塊: 1:try 代碼塊,用來標記須要進行異常監控的代碼。 2:catch 代碼塊,跟在 try 代碼塊以後,用來捕獲在 try 代碼塊中觸發的某種指定類型的異常。除了聲明所捕獲異常的類型以外,catch 代碼塊還定義了針對該異常類型的異常處理。在 Java 中,try 代碼塊後面能夠跟着多個 catch 代碼塊,來捕獲不一樣類型的異常。Java 虛擬機會自上而下匹配異常處理器,因此前面的 catch 代碼塊不能覆蓋後面的,不然編譯器報錯。 3:finally 代碼塊,跟在 catch 代碼塊以後,用來聲明一段一定運行的代碼。它的設計是爲了不跳過某些關鍵的清理代碼。好比關閉已經打開的系統資源。緩存
在程序正常執行狀況下, finally 代碼塊會在 try 代碼塊以後運行。架構
若是 try 代碼塊觸發異常,異常沒有被捕獲的狀況下,finally 代碼塊會直接運行,並在運行結束後從新拋出異常。若是該異常被 catch 代碼塊捕獲,finally 代碼塊則會在 catch 代碼塊以後運行。在某些狀況下,catch 代碼塊也觸發了異常,那麼 finally 代碼一樣會執行,並拋出 catch 代碼塊觸發的異常。若是 finally 代碼塊也觸發了異常,那就中斷 finally 代碼塊,向上拋出異常。性能
在 Java 語言規範中,全部異常都是 Throwable 類或者其子類的實現。優化
Throwable 類有兩大直接子類。一個是 Error,涵蓋程序不該捕獲的異常。當程序觸發 Error 的時候,它的執行狀態已經沒法會發,須要終止線程甚至是終止虛擬機。一個是 Exception ,涵蓋程序可能須要捕獲而且處理的異常。spa
RuntimeException 是 Exception 的一個特殊子類,用來表示程序沒法繼續執行,可是還能搶救一下的狀況,數組越界即是其中一種。線程
RuntimeException 和 Error 屬於 Java 裏的非檢查異常。其餘異常則屬於檢查異常。在Java 語法中,全部的檢查異常都須要程序顯示地捕獲,或者在方法聲明中用 throws 關鍵字標註。一般狀況下,程序自定義的異常應爲檢查異常,以便最大化利用 Java 編譯器的編譯時檢查。設計
異常實例的構造十分昂貴。在構造異常實例時,Java 虛擬機須要生成該異常的棧軌跡。該曹組會逐一訪問當前線程的 Java 棧幀,而且記錄下各類調試信息,包括棧幀所指向的方法的名字,方法所在的類名,文件名,以及在代碼中的第幾行觸發該異常。在生成棧軌跡時,Java 虛擬機會忽略掉異常構造器以及填充棧幀的 Java 方法,直接重新建異常位置開始算起。此外,Java 虛擬機還會忽略標記爲不可見的 Java 方法棧幀。指針
異常實例的構造昂貴,可是卻沒有作緩存優化。若是作了緩存優化,那麼拋出的異常實例對應的棧軌跡並不是 throw 語句的位置了,而是第一次新建異常的位置。因此,爲了準確的定位到錯誤的位置,咱們每每選擇拋出新建異常實例。
在編譯生成的字節碼中,每一個方法都附帶一個異常表,異常表中的每個條目表明一個異常處理器,而且由 form 指針,to 指針,target 指針以及所捕獲的異常類型構成。這些指針的值是字節碼索引,用來定位字節碼。
from 指針和 to 指針標示了該異常所監控的範圍:try 代碼塊所覆蓋的範圍。 target 指針標示了異常處理器的起始位置:catch 代碼塊的起始位置。
當程序處罰異常時,Java 虛擬機會從上至下遍歷異常表中的全部條目。當觸發異常的字節碼索引值在某個異常表條目的監控範圍內,Java 虛擬機再判斷所拋出的異常和該條目想要捕獲的異常是否匹配。若是匹配,Java 虛擬機會將控制流轉移至該條目 target 指針指向的字節碼。
若是遍歷完異常表的條目不曾匹配到異常處理器,那麼它會彈出當前方法對應的 Java 棧幀,而且在調用者中重複上述操做。
finally 代碼塊的編譯比較複雜。當前版本 Java 編譯器的作法:複製 finally 代碼塊內容,分別放在 try-catch 代碼塊全部正常執行路徑以及異常執行路徑的出口中。
針對異常執行路徑,Java 編譯器會生成一個(上圖變種2)或者多個(上圖變種1)異常表條目,監控整個 try-catch 代碼塊,而且捕獲全部種類的異常。這些異常表條目的 target 指針將指向另外一份複製的 finally 代碼塊(上圖變種1,變種2 中紅色 finally block),而且從新拋出捕獲的異常。
問題:若是 catch 代碼塊捕獲了異常,而且觸發了另外一個異常,那麼 finally 捕獲而且重拋的異常是 catch 代碼塊觸發的新的異常,本來的異常就被忽略了。這對代碼調試來講,就不友好了。
Java 7 中引出了 Supressed 異常來解決上面的問題。這個新特性容許開發人員將一個異常附在另外一個異常上,這樣拋出的異常就能夠附帶多個異常的信息。
單從 Java 語法上看不出來,可是從 JVM 實現的細節上來看就明白了。構造異常實例,須要生成該異常的棧軌跡。該操做會逐一訪問當前線程的棧幀,記錄各類調試信息,包括類名,方法名,觸發異常的代碼行數等等。
編譯器在編譯代碼時會複製 finally 代碼塊放在 try-catch 代碼塊全部正常執行路徑以及異常執行路徑的出口處。
catch 拋出的異常會被 finally 捕獲,執行完 finally 後會從新拋出該異常。因爲 finally 中有 return 語句,在從新拋出異常以前,代碼就已經返回了。
方法的異常表只聲明這段代碼會被捕獲的異常,並且是非檢查異常。若是 catch 中有自定義異常,那麼異常表中也會包含自定義異常的條目。
檢查異常也會在運行過程當中拋出。可是它會要求編譯器檢查代碼有沒有顯式地處理該異常。非檢查異常包括Error和RuntimeException,這兩個則不要求編譯器顯式處理。
本文創做靈感來源於 極客時間 鄭雨迪老師的《深刻拆解 Java 虛擬機》課程,經過課後反思以及借鑑各位學友的發言總結,現整理出本身的知識架構,以便往後溫故知新,查漏補缺。
原文出處:https://www.cnblogs.com/yuepenglei/p/10290161.html