聊聊Java的異常機制及實現

前言

在一些傳統的編程語言,如C語言中,並無專門處理異常的機制,程序員一般用方法的特定返回值來表示異常狀況,而且程序的正常流程和異常流程都採用一樣的流程控制語句。
Java語言按照面向對象的思想來處理異常,使得程序具備更好的可維護性。Java異常處理機制具備如下優勢:html

  1. 把各類不一樣類型的異常狀況進行分類,用Java類來表示異常狀況,這種類被稱爲異常類。把異常狀況表示成異常類,能夠充分發揮類的可擴展和可重用的優點。java

  2. 異常流程的代碼和正常流程的代碼分離,提升了程序的可讀性,簡化程序的結構。程序員

  3. 能夠靈活的處理異常,若是當前方法有能力處理異常,就捕獲並處理它,不然只須要拋出異常,由方法調用。編程

Java異常基礎

關於異常的使用我就再也不多說了,在這裏仍是先提幾個問題:數組

  • catch多個異常的時候,按什麼規則選擇呢架構

  • throws異常是不是函數簽名的一部分呢jvm

  • 覆蓋父類的帶throws的函數是否也須要加throws呢編程語言

  • 同時實現多個接口中同名拋出異常的函數最後拋出異常的集合是什麼呢函數

接下來咱們回答其中的部分問題,先看一個例子spa

異常捕獲示例

能夠看到Java是按照catch聲明的順序來捕獲異常的,且編譯器不容許將父類異常聲明在子類以前。

throws異常顯然不是函數的一部分,由於兩個throws不一樣的同名同參數的函數不容許重載。

覆蓋時的異常聲明示例

從上圖咱們能夠看出覆蓋對拋出異常的聲明並無要求。

throws的接口繼承

上圖能夠看出編譯器對接口的方法實現也並沒有什麼要求,重點在於try-catch塊的檢查,你不能catch一個你在throw塊裏不可能拋出的檢查類型異常,而這種判斷是經過你調用方法聲明的拋出異常,即便你在方法實現裏不可能拋出該異常,你加在throws裏,同樣能夠矇騙編譯器。對於方法聲明的拋出異常,只有一個條件須要知足,那就是你的實現中可能拋出的檢查類型異常要麼處理要麼聲明拋出,不須要考慮繼承和實現關係給throws帶來的影響,這是參考文章中的一點小錯誤,特此更正。

Java異常類的架構

Java異常架構圖

  1. Throwable

  • Throwable是 Java 語言中全部錯誤或異常的超類。

  • Throwable包含兩個子類: Error 和 Exception。它們一般用於指示發生了異常狀況。

  • Throwable包含了其線程建立時線程執行堆棧的快照,它提供了printStackTrace()等接口用於獲取堆棧跟蹤數據等信息。

  1. Exception

  • Exception及其子類是 Throwable 的一種形式,它指出了合理的應用程序想要捕獲的條件。

  1. RuntimeException

  • RuntimeException是那些可能在 Java 虛擬機正常運行期間拋出的異常的超類。

  • 編譯器不會檢查RuntimeException異常。例如,除數爲零時,拋出ArithmeticException異常。RuntimeException是ArithmeticException的超類。當代碼發生除數爲零的狀況時,假若既"沒有經過throws聲明拋出ArithmeticException異常",也"沒有經過try...catch...處理該異常",也能經過編譯。這就是咱們所說的"編譯器不會檢查RuntimeException異常"!

  • 若是代碼會產生RuntimeException異常,則須要經過修改代碼進行避免。例如,若會發生除數爲零的狀況,則須要經過代碼避免該狀況的發生!

  1. Error

  • 和Exception同樣,Error也是Throwable的子類。它用於指示合理的應用程序不該該試圖捕獲的嚴重問題,大多數這樣的錯誤都是異常條件。

  • 和RuntimeException同樣,編譯器也不會檢查Error。

Java將可拋出(Throwable)的結構分爲三種類型:被檢查的異常(Checked Exception),運行時異常(RuntimeException)和錯誤(Error)。

(01) 運行時異常

  • 定義: RuntimeException及其子類都被稱爲運行時異常。

  • 特色: Java編譯器不會檢查它。也就是說,當程序中可能出現這類異常時,假若既"沒有經過throws聲明拋出它",也"沒有用try-catch語句捕獲它",仍是會編譯經過。例如,除數爲零時產生的ArithmeticException異常,數組越界時產生的IndexOutOfBoundsException異常,fail-fail機制產生的ConcurrentModificationException異常等,都屬於運行時異常。

  • 雖然Java編譯器不會檢查運行時異常,可是咱們也能夠經過throws進行聲明拋出,也能夠經過try-catch對它進行捕獲處理。

  • 若是產生運行時異常,則須要經過修改代碼來進行避免。例如,若會發生除數爲零的狀況,則須要經過代碼避免該狀況的發生!

(02) 被檢查的異常

  • 定義: Exception類自己,以及Exception的子類中除了"運行時異常"以外的其它子類都屬於被檢查異常。

  • 特色: Java編譯器會檢查它。此類異常,要麼經過throws進行聲明拋出,要麼經過try-catch進行捕獲處理,不然不能經過編譯。例如,CloneNotSupportedException就屬於被檢查異常。當經過clone()接口去克隆一個對象,而該對象對應的類沒有實現Cloneable接口,就會拋出CloneNotSupportedException異常。

  • 被檢查異常一般都是能夠恢復的。

(03) 錯誤

  • 定義: Error類及其子類。

  • 特色: 和運行時異常同樣,編譯器也不會對錯誤進行檢查。

  • 當資源不足、約束失敗、或是其它程序沒法繼續運行的條件發生時,就產生錯誤。程序自己沒法修復這些錯誤的。例如,VirtualMachineError就屬於錯誤。

  • 按照Java慣例,咱們是不該該是實現任何新的Error子類的!

經常使用異常舉例

Java異常的實現原理

異常的捕獲原理

首先介紹下java的異常表(Exception table),異常表是JVM處理異常的關鍵點,在java類中的每一個方法中,會爲全部的try-catch語句,生成一張異常表,存放在字節碼的最後,該表記錄了該方法內每一個異常發生的起止指令和處理指令。

接下來看一個例子:

public void catchException() {  
    long l = System.nanoTime();  
    for (int i = 0; i < testTimes; i++) { 
        try {  
            throw new Exception();  
        } catch (Exception e) { 
            //nothing to do
        }  
    }
    System.out.println("拋出並捕獲異常:" + (System.nanoTime() - l));  
}

字節碼以下

字節碼示例

面請結合java代碼和生成的字節碼來看下面的指令分析:
0-4號: 執行try前面的語句
5號: 執行try語句前保存現場
6號: 執行try語句後跳轉指令行,圖中表示跳轉到22
9-17號: try-catch代碼生成指令,結合紅色框圖異常表,表示9-17號指令如有Exception異常拋出就執行17行指令.
16號: athrow 表示拋出異常
17號: astore 表示jvm將該異常實例存儲到局部變量表中方便一旦出方法棧調用方能夠找到
22號: 恢復try語句執行前保存的現場
對比指令分析,再結合使用try-catch代碼分析:

  • 若try沒有拋出異常,則繼續執行完try語句,跳過catch語句,此時就是從指令6跳轉到指令22.

  • 若try語句拋出異常則執行指令17,將異常保存起來,若異常被方法拋出,調用方拿到異常可用於異常層次索引。

經過以上的分析,能夠知道JVM是怎麼捕獲並處理異常,其實就是使用goto指令來作上下文切換。

異常的處理機制

上面大體介紹了異常是如何產生並捕獲的,接下來咱們詳細講講athrow指令拋出異常後的故事,也就是如何處理異常的問題。

athrow指令,這個指令運做過程大體是首先檢查操做棧頂,這時棧頂必須存在一個reference類型的值,而且是java.lang.Throwable的子類(虛擬機規範中要求若是遇到null則看成NPE異常使用),而後暫時先把這個引用出棧,接着搜索本方法的異常表,找一下本方法中是否有能處理這個異常的handler,若是能找到合適的handler就會從新初始化PC寄存器指針指向此異常handler的第一個指令的偏移地址。接着把當前棧幀的操做棧清空,再把剛剛出棧的引用從新入棧。若是在當前方法中很悲劇的找不到handler,那隻好把當前方法的棧幀出棧(這個棧是VM棧,不要和前面的操做棧搞混了,棧幀出棧就意味着當前方法退出),這個方法的調用者的棧幀就天然在這條線程VM棧的棧頂了,而後再對這個新的當前方法再作一次剛纔作過的異常handler搜索,若是仍是找不到,繼續把這個棧幀踢掉,這樣一直到找,要麼找到一個能使用的handler,轉到這個handler的第一條指令開始繼續執行,要麼把VM棧的棧幀拋光了都沒有找到指望的handler,這樣的話這條線程就只好被迫終止、退出了。

對於Java語言中的關鍵字catch和finally,虛擬機中並無特殊的字節碼指令去支持它們,都是經過編譯器生成字節碼片斷以及不一樣的異常處理器來實現。

咱們總結一下athrow指令中虛擬機可能作的事情:

  • 檢查棧頂異常對象類型(只檢查是否是null,是否referance類型,是否Throwable的子類通常在類驗證階段的數據流分析中作,或者索性不作靠編譯器保證了,編譯時寫到Code屬性的StackMapTable中,在加載時僅作類型驗證)

  • 把異常對象的引用出棧

  • 搜索異常表,找到匹配的異常handler

  • 重置PC寄存器狀態

  • 清理操做棧

  • 把異常對象的引用入棧

  • 把異常方法的棧幀逐個出棧(這裏的棧是VM棧)

  • 殘忍地終止掉當前線程。

異常到底慢不慢

這裏直接給出一些結論吧:

新建一個異常對象比新建一個普通對象在耗時上多一個數量級,拋出並捕獲異常的耗時比新建一個異常在耗時上也要多一個數量級。建立一個異常對象倒是要比一個普通對象耗時多,捕獲一個異常耗時更甚。捕獲的過程咱們上面已經簡要介紹了,爲何新建一個異常對象這麼耗時?且看源碼:

在java中,全部的異常都繼承自Throwable類,Throwable的構造函數

public Throwable() {
    ...
    fillInStackTrace();
    ...
}

有個nativ方法public synchronized native Throwable fillInStackTrace();這個方法會存入當前線程的堆棧信息。也就是說每次建立一個異常實例都會把堆棧信息存一遍。這就是時間開銷的主要來源了。

這個時候咱們能夠下一個結論:新建異常對象比建立一個普通對象是要更加的耗時。

能避開建立異常的這個耗時嗎?答案是能夠的,若是在程序中咱們不關心異常拋出的異常佔信息,咱們能夠本身定義一個異常繼承自已有的異常類型,並寫一個方法覆蓋掉fillInStackTrace方法就好了。

參考文章

相關文章
相關標籤/搜索