今天咱們來討論一下,程序中的錯誤處理。java
在任何一個穩定的程序中,都會有大量的代碼在處理錯誤,有一些業務錯誤,咱們能夠經過主動檢查判斷來規避,可對於一些不能主動判斷的錯誤,例如 RuntimeException,咱們就須要使用 try-catch-finally
語句了。小程序
有人說,錯誤處理並不難啊,try-catch-finally
一把梭,try 放功能代碼,在 catch 中捕獲異常、處理異常,finally 中寫那些不管是否發生異常,都要執行的代碼,這很簡單啊。app
處理錯誤的代碼,確實並不難寫,但是想把錯誤處理寫好,也並非一件容易的事情。異步
接下來咱們就從實現到 JVM 原理,講清楚 Java 的異常處理。jvm
學東西,我仍是推薦要帶着問題去探索,提早思考幾個問題吧:函數
既然是異常處理,確定是區分異常發生和捕獲、處理異常,這也正是組成異常處理的兩大要素。佈局
在 Java 中,拋出的異常能夠分爲顯示異常和隱式異常,這種區分主要來自拋出異常的主體是什麼,顯示和隱式也是站在應用程序的視角來區分的。性能
顯示異常的主體是當前咱們的應用程序,它指的是在應用程序中使用 「throw」 關鍵字,主動將異常實例拋出。而隱式異常就不受咱們控制, 它觸發的主體是 Java 虛擬機,指的是 Java 虛擬機在執行過程當中,遇到了沒法繼續執行的異常狀態,續而將異常拋出。學習
對於隱式異常,在觸發時,須要顯示捕獲(try-catch),或者在方法頭上,用 "throw" 關鍵字聲明,交由調用者捕獲處理。 編碼
在咱們編寫異常處理代碼的時候,主要就是使用前面介紹到的 try-catch-finally
這三種代碼塊。
catch 容許存在多個,用於針對不一樣的異常作不一樣的處理。若是使用 catch 捕獲多種異常,各個 catch 塊是互斥的,和 switch 語句相似,優先級是從上到下,只能選擇其一去處理異常。
既然 try-catch-finally 存在多種狀況,而且在發生異常和不發生異常時,表現是不一致的,咱們就分清楚來單獨分析。
1. try塊中,未發生異常
不觸發異常,固然是咱們樂於看見的。在這種狀況下,若是有 finally 塊,它會在 try 塊以後運行,catch 塊永遠也不會被運行。
2. try塊中,發生異常
在發生異常時,會首先檢查異常類型,是否存在於咱們的 catch 塊中指定的待捕獲異常。若是存在,則這個異常被捕獲,對應的 catch 塊代碼則開始運行,finally 塊代碼緊隨其後。
例如:咱們只監聽了空指針(NullPointerException),此時若是發生了除數爲 0 的崩潰(ArithmeticException),則是不會被處理的。
當觸發了咱們未捕獲的異常時,finally 代碼依然會被執行,在執行完畢後,繼續將異常「拋出去」。
3. catch 或者 finally 發生異常
catch 代碼塊和 finally 代碼塊,也是咱們編寫的,理論上也是有出錯的可能。
那麼這兩段代碼發生異常,會出現什麼狀況呢?
當在 catch 代碼塊中發生異常時,此時的表現取決於 finally 代碼塊中是否存在 return 語句。若是存在,則 finally 代碼塊的代碼執行完畢直接返回,不然會在 finally 代碼塊執行完畢後,將 catch 代碼中新產生的異常,向外拋出去。
而在極端狀況下,finally 代碼塊發生了異常,則此時會中斷 finally 代碼塊的執行,直接將異常向外拋出。
再回頭看看第一個問題,假如咱們寫了一個方法,其中的代碼被 try-catch-finally
包裹住進行異常處理,此時若是咱們在多個地方都有 return 語句,最終誰的會被執行?
如上圖所示,在完整的 try-catch-finally
語句中,finally 都是最後執行的,假設 finally 代碼塊中存在 return 語句,則直接返回,它是優先級最高的。
通常咱們不建議在 finally 代碼塊中添加 return 語句,由於這會破壞並阻止異常的拋出,致使不宜排查的崩潰。
在 Java 中,全部的異常,其實都是一個個異常類,它們都是 Throwable 類或其子類的實例。
Throwable 有兩大子類,Exception 和 Error。
一般,咱們只須要捕獲 Exception 就能夠了。但 Exception 中,有一個特殊的子類 RuntimeException,即運行時錯誤,它是在程序運行時,動態出現的一些異常。比較常見的就是 NullPointerException、ArrayIndexOutOfBoundsException 等。
Error 和 RuntimeException 都屬於非檢查異常(Unchecked Exception),與之相對的就是普通 Exception 這種屬於檢查異常(Checked Exception)。
全部檢查異常都須要在程序中,用代碼顯式捕獲,或者在方法中用 throw 關鍵字顯式標註。其實意思很明顯,要不你本身處理了,要不你拋出去讓別人處理。
這種檢查異常的機制,是在編譯期間進行檢查的,因此若是不按此規範處理,在編譯器編譯代碼時,就會拋出異常。
對於異常處理的性能問題,實際上是一個頗有爭議的問題,有人以爲異常處理是多作了一些工做,確定對性能是有影響的。可是也有人以爲異常處理的影響,和增長一個 if-else
屬於同種量級,對性能的影響其實微乎其微,是在能夠接受的範圍內的。
既然有爭議,最簡單的辦法是寫個 Demo 驗證一下。固然,咱們這裏是須要區分不一樣的狀況,而後根據解決對比的。
一個最簡單的 for 循環 100w 次,在其中作一個 a++
的自增操做。
try-catch
語句。a++
包在 try
代碼塊中。try
代碼塊中,觸發一個異常。就是一個簡單的 for 循環,就不貼代碼了,異常經過 5/0
這樣的運算,觸發除數爲 0 的 ArithmeticException 異常,並在 JDK 1.8 的環境下運行。
爲了不影響採樣結果,每一個例子都單獨運行 10 遍以後,取平均值(單位納秒)。
到這裏基本上就能夠得出結論了,在沒有發生異常的狀況下,try-catch 對性能的影響微乎其微。可是一旦發生異常,性能上則是災難性的。
所以,咱們應該儘量的避免經過異常來處理正常的邏輯檢查,這樣能夠確保不會由於發生異常而致使性能問題。
至於爲何發生異常時,性能差異會有如此之大,就須要從 Java 虛擬機 JVM 的角度來分析了,後面會詳細分析。
try-catch-finally
確實很好用,可是它並不能捕獲,異步回調中的異常。try 語句裏的方法,若是容許在另一個線程中,其中拋出的異常,是沒法在調用者這個線程中捕獲的。
這一點在使用的過程當中,須要特別注意。
接下來咱們從 JVM 的角度,分析 JVM 如何處理異常。
當異常發生時,異常實例的構建,是很是消耗性能的。這是因爲在構造異常實例時,Java 虛擬機須要生成該異常的異常棧(stack trace)。
異常棧會逐一訪問當前線程的 Java 棧幀,以及各類調試信息。包括棧幀所指向的方法名,方法所在的類名、文件名以及在代碼中是第幾行觸發的異常。
這些異常輸出到 Log 中,就是咱們熟悉的崩潰日誌(崩潰棧)。
當把 Java 代碼編譯成字節碼後,每一個方法都會附帶一個異常表,其中記錄了當前方法的異常處理。
下面直接舉個例子,寫一個最簡單的 try-catch
類。
使用 javap -c
進行反編譯成字節碼。
能夠看到,末尾的 Exceptions Table 就是異常表。異常表中的每一條記錄,都表明了一個異常處理器。
異常處理器中,標記了當前異常監控的起始、結束代碼索引,和異常處理器的索引。其中 from 指針和 to 指針標識了該異常處理器所監控的代碼範圍,target 指針則指向異常處理器的起始位置,type 則爲最後監聽的異常。
例如上面的例子中,main 函數中存在異常表,Exception 的異常監聽代碼範圍分別是 [0,8)(不包括 8),異常處理器的索引爲 11。
繼續分析異常處理流程,還須要區分是否命中異常。
1. 命中異常
當程序發生異常時,Java 虛擬機會從上到下遍歷異常表中全部的記錄。當發現觸發異常的字節碼的索引值,在某個異常表中某個異常監控的範圍內。Java 虛擬機會判斷所拋出的異常和該條異常監聽的異常類型,是否匹配。若是能匹配上,Java 虛擬機會將控制流轉向至該此異常處理器的 target 索引指向的字節碼,這是命中異常的狀況。
2. 未命中異常
而若是遍歷完異常表中全部的異常處理器以後,仍未匹配到異常處理器,那麼它會彈出當前方法對應的 Java 棧幀。回到它的調用者,在其中重複此過程。
最壞的狀況下,Java 虛擬機須要遍歷當前線程 Java 棧上全部方法的異常表。
咱們寫的代碼,其實終歸是給人讀的,可是編譯器乾的事兒,都不是人事兒。它會把代碼作一些特殊的處理,只是爲了讓本身更好解析和執行。
編譯器對 finally 代碼塊,就是這樣處理的。在當前版本的 Java 編譯器中,會將 finally 代碼塊的內容,複製幾份,分別放在全部可能執行的代碼路徑的出口中。
寫個 Demo 驗證一下,代碼以下。
繼續 javap -c
反編譯成字節碼。
這個例子中,爲了更清晰的看到 finally 代碼塊,我在其中輸出的一段 Log 「run finally」。能夠看到,編譯結果中,包含了三份 finally 代碼塊。
其中,前兩份分別位於 try 代碼塊和 catch 代碼塊的正常執行路徑出口。最後一份則做爲全局的異常處理器,監控 try 代碼塊以及 catch 代碼塊。它將捕獲 try 代碼塊觸發而且未命中 catch 代碼塊捕獲的異常,以及在 catch 代碼塊觸發的異常。
而 finally 的代碼,若是出現異常,就不是當前方法所能處理的了,會直接向外拋出。
從上圖中能夠看到,在異常表中,還存在兩個 any 的信息。
第一個信息的 from 和 to 的範圍就是 try 代碼塊,等因而對 catch 遺漏異常的一種補充,表示會處理全部種類的異常。
第二個信息的 from 和 to 的範圍,仔細看能看到它實際上是 catch 代碼塊,這也正好印證了咱們上面的結論,catch 代碼塊其實也被異常處理器監控着。
只是若是命中了 any 以後,由於沒有對應的異常處理器,會繼續向上拋出去,交由該方法的調用方法處理。
到這裏咱們就基本上講清楚了 Java 異常處理的全部內容。
在平常開發當中,應該儘可能避免使用異常處理的機制來處理業務邏輯,例如不少代碼中,類型轉換就使用 try-catch
來處理,實際上是很不可取的。
異常捕獲對應用程序的性能確實有影響,但也是分狀況的。
一旦異常被拋出來,方法也就跟着 return 了,捕獲異常棧時會致使性能變得很慢,尤爲是調用棧比較深的時候。
可是從另外一個角度來講,異常拋出時,基本上代表程序的錯誤。應用程序在大多數狀況下,應該是在沒有異常狀況的環境下運行的。因此,異常狀況應該是少數狀況,只要咱們不濫用異常處理,基本上不會影響正常處理的性能問題。
本文對你有幫助嗎?留言、點贊、轉發是最大的支持,謝謝!
「聯機圓桌」👈推薦個人知識星球,一年 50 個優質問題,上桌聯機學習。
公衆號後臺回覆成長『成長』,將會獲得我準備的學習資料,也能回覆『加羣』,一塊兒學習進步;你還能回覆『提問』,向我發起提問。
推薦閱讀:
「寒冬」正是學習時|關於字符編碼,你須要知道的都在這裏 | 分詞,科普及解決方案| 圖解:HTTP 範圍請求 | 小程序學習資料 |HTTP 內容編碼 | 輔助模式實戰 | 輔助模式玩出花樣 | 小程序 Flex 佈局