[TOC]
# Java異常淺談
全文轉載至公眾號[承香墨影-“崩潰了?不可能,我全 Catch 住了”][ref-崩潰了-不可能-我全catch住了]
## 前言
在任何一個穩定的程序中,都會有大量的代碼在處理錯誤,有一些業務錯誤,我們可以通過主動檢查判斷來規避,可對于一些不能主動判斷的錯誤,例如 RuntimeException,我們就需要使用`try-catch-finally`語句了。
今天我們來討論一下,程序中的錯誤處理。
有人說,錯誤處理并不難啊,`try-catch-finally`一把梭,**try**放功能代碼,在**catch**中捕獲異常、處理異常,**finally**中寫那些無論是否發生異常,都要執行的代碼,這很簡單啊。

處理錯誤的代碼,確實并不難寫,可是想把錯誤處理寫好,也并不是一件容易的事情。
接下來我們就從實現到 JVM 原理,講清楚 Java 的異常處理。
學東西,我還是推薦要帶著問題去探索,提前思考幾個問題吧:
1. 一個方法,異常捕獲塊中,不同的地方的 return 語句,誰會生效?
2. catch 和 finally 中出現異常,會如何處理?
3. try-catch 是否影響效率?
4. Java 異常捕獲的原理?
## 二、Java 異常處理
### 2.1 概述
既然是異常處理,肯定是區分**異常發生**和**捕獲、處理異常**,這也正是組成異常處理的兩大要素。
在 Java 中,拋出的異常可以分為**顯示異常**和**隱式異常**,這種區分主要來自拋出異常的主體是什么,顯示和隱式也是站在應用程序的視角來區分的。
顯示異常的主體是當前我們的應用程序,它指的是在應用程序中使用 “throw” 關鍵字,主動將異常實例拋出。而隱式異常就不受我們控制, 它觸發的主體是 Java 虛擬機,指的是 Java 虛擬機在執行過程中,遇到了無法繼續執行的異常狀態,續而將異常拋出。

對于隱式異常,在觸發時,需要顯示捕獲(try-catch),或者在方法頭上,用 "throw" 關鍵字聲明,交由調用者捕獲處理。
### 2.2 使用異常捕獲
在我們編寫異常處理代碼的時候,主要就是使用前面介紹到的`try-catch-finally`這三種代碼塊。

* try 代碼塊:包含待監控異常的代碼。
* catch 代碼塊:緊跟 try 塊之后,可以指定異常類型。允許指定捕獲多種不同的異常,catch 塊用來捕獲在 try 塊中出發的某個指定類型的異常。
* finally 代碼塊:緊跟 try 塊或 catch 塊之后,用來聲明一段必定會運行的代碼。例如用來清理一些資源。
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 代碼塊的執行,直接將異常向外拋出。
### 2.3 異常捕獲的返回值
再回頭看看第一個問題,假如我們寫了一個方法,其中的代碼被`try-catch-finally`包裹住進行異常處理,此時如果我們在多個地方都有 return 語句,最終誰的會被執行?

如上圖所示,在完整的`try-catch-finally`語句中,finally 都是最后執行的,假設 finally 代碼塊中存在 return 語句,則直接返回,它是優先級最高的。
一般我們不建議在 finally 代碼塊中添加 return 語句,因為這會破壞并阻止異常的拋出,導致不宜排查的崩潰。
### 2.4 異常的類型
在 Java 中,所有的異常,其實都是一個個異常類,它們都是 Throwable 類或其子類的實例。

Throwable 有兩大子類,**Exception**和**Error**。
* Exception:表示程序可能需要捕獲并且處理的異常。
* Error:表示當觸發 Error 時,它的執行狀態已經無法恢復了,需要中止線程甚至是中止虛擬機。這是不應該被我們應用程序所捕獲的異常。
通常,我們只需要捕獲 Exception 就可以了。但 Exception 中,有一個特殊的子類 RuntimeException,即運行時錯誤,它是在程序運行時,動態出現的一些異常。比較常見的就是 NullPointerException、ArrayIndexOutOfBoundsException 等。
Error 和 RuntimeException 都屬于非檢查異常(Unchecked Exception),與之相對的就是普通 Exception 這種屬于檢查異常(Checked Exception)。
所有檢查異常都需要在程序中,用代碼顯式捕獲,或者在方法中用 throw 關鍵字顯式標注。其實意思很明顯,要不你自己處理了,要不你拋出去讓別人處理。
這種檢查異常的機制,是在編譯期間進行檢查的,所以如果不按此規范處理,在編譯器編譯代碼時,就會拋出異常。
### 2.5 異常處理的性能問題
對于異常處理的性能問題,其實是一個很有爭議的問題,有人覺得異常處理是多做了一些工作,肯定對性能是有影響的。但是也有人覺得異常處理的影響,和增加一個`if-else`屬于同種量級,對性能的影響其實微乎其微,是在可以接受的范圍內的。
既然有爭議,最簡單的辦法是寫個 Demo 驗證一下。當然,我們這里是需要區分不同的情況,然后根據解決對比的。
一個最簡單的 for 循環 100w 次,在其中做一個`a++`的自增操作。
* A:無任何`try-catch`語句。
* B:將`a++`包在`try`代碼塊中。
* C:在`try`代碼塊中,觸發一個異常。
就是一個簡單的 for 循環,就不貼代碼了,異常通過`5/0`這樣的運算,觸發除數為 0 的 ArithmeticException 異常,并在 JDK 1.8 的環境下運行。
為了避免影響采樣結果,每個例子都單獨運行 10 遍之后,取平均值(單位納秒)。

到這里基本上就可以得出結論了,**在沒有發生異常的情況下,try-catch 對性能的影響微乎其微。但是一旦發生異常,性能上則是災難性的。**
因此,我們應該盡可能的避免通過異常來處理正常的邏輯檢查,這樣可以確保不會因為發生異常而導致性能問題。
至于為什么發生異常時,性能差別會有如此之大,就需要從 ?Java 虛擬機 JVM 的角度來分析了,后面會詳細分析。
### 2.6 異常處理無法覆蓋異步回調
`try-catch-finally`確實很好用,但是它并不能捕獲,異步回調中的異常。try 語句里的方法,如果允許在另外一個線程中,其中拋出的異常,是無法在調用者這個線程中捕獲的。
這一點在使用的過程中,需要特別注意。
## 三、JVM 如何處理異常
### 3.1 JVM 異常處理概述
接下來我們從 JVM 的角度,分析 JVM 如何處理異常。
當異常發生時,異常實例的構建,是非常消耗性能的。這是由于在構造異常實例時,Java 虛擬機需要生成該異常的異常棧(stack trace)。
異常棧會逐一訪問當前線程的 Java 棧幀,以及各種調試信息。包括棧幀所指向的方法名,方法所在的類名、文件名以及在代碼中是第幾行觸發的異常。
這些異常輸出到 Log 中,就是我們熟悉的崩潰日志(崩潰棧)。
### 3.2 崩潰實例分析異常處理
當把 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 棧上所有方法的異常表。
### 3.3 編譯后的 finally 代碼塊
我們寫的代碼,其實終歸是給人讀的,但是編譯器干的事兒,都不是人事兒。它會把代碼做一些特殊的處理,只是為了讓自己更好解析和執行。
編譯器對 finally 代碼塊,就是這樣處理的。在當前版本的 Java 編譯器中,會將 finally 代碼塊的內容,復制幾份,分別放在所有可能執行的代碼路徑的出口中。

寫個 Demo 驗證一下,代碼如下。

繼續`javap -c`反編譯成字節碼。

這個例子中,為了更清晰的看到 finally 代碼塊,我在其中輸出的一段 Log “run finally”。可以看到,編譯結果中,包含了三份 finally 代碼塊。
其中,前兩份分別位于 try 代碼塊和 catch 代碼塊的正常執行路徑出口。最后一份則作為全局的異常處理器,監控 try 代碼塊以及 catch 代碼塊。它將捕獲 try 代碼塊觸發并且未命中 catch 代碼塊捕獲的異常,以及在 catch 代碼塊觸發的異常。
而 finally 的代碼,如果出現異常,就不是當前方法所能處理的了,會直接向外拋出。
### 3.4 異常表中的 any 是什么?
從上圖中可以看到,在異常表中,還存在兩個 any 的信息。

第一個信息的 from 和 to 的范圍就是 try 代碼塊,等于是對 catch 遺漏異常的一種補充,表示會處理所有種類的異常。
第二個信息的 from 和 to 的范圍,仔細看能看到它其實是 catch 代碼塊,這也正好印證了我們上面的結論,catch 代碼塊其實也被異常處理器監控著。
只是如果命中了 any 之后,因為沒有對應的異常處理器,會繼續向上拋出去,交由該方法的調用方法處理。
## 四、總結
到這里我們就基本上講清楚了 Java 異常處理的所有內容。
在日常開發當中,應該盡量避免使用異常處理的機制來處理業務邏輯,例如很多代碼中,類型轉換就使用`try-catch`來處理,其實是很不可取的。
異常捕獲對應用程序的性能確實有影響,但也是分情況的。
一旦異常被拋出來,方法也就跟著 return 了,捕獲異常棧時會導致性能變得很慢,尤其是調用棧比較深的時候。
但是從另一個角度來說,異常拋出時,基本上表明程序的錯誤。應用程序在大多數情況下,應該是在沒有異常情況的環境下運行的。所以,異常情況應該是少數情況,只要我們不濫用異常處理,基本上不會影響正常處理的性能問題。

## 文獻來源
[ref-崩潰了-不可能-我全catch住了]:https://mp.weixin.qq.com/s?__biz=MzIxNjc0ODExMA==&mid=2247485952&idx=1&sn=26ba1c2c8342062d82f3b5487839753d&chksm=97851321a0f29a37fe2e2f72bb3448cb7b141ac30b7b715ec126583e8c6fa2c5da76ec83c6b6&mpshare=1&scene=1&srcid=01278BYIkXoMVn0zohrfLBOu#rd
- 計算機基礎
- 簡答1
- 簡答2
- 專案
- 淺談0與1
- 淺談TCP_IP
- 淺談HTTP
- 淺談HTTPS
- 數據結構與算法
- 常見數據結構簡介
- 常用算法分析
- 常見排序算法
- Java數據結構類問題簡答
- 專案
- HashMap
- 淺談二叉樹
- 算法題
- 算法001_TopN問題
- 算法002_漢諾塔
- 編程思想
- 雜說
- 觀點_優秀程序設計的18大原則
- 設計模式_創建型
- 1_
- 2_
- 設計模式_結構型
- 1_
- 2_
- 設計模式_行為型
- 1_
- 2_
- Java相關
- 簡答1
- 簡答2
- 專案
- 淺談String
- 淺談Java泛型
- 淺談Java異常
- 淺談動態代理
- 淺談AOP編程
- 淺談ThreadLocal
- 淺談Volatile
- 淺談內存模型
- 淺談類加載
- 專案_數據結構
- 淺談SpareArray
- Android相關
- Android面試題
- 專案
- 推送原理解析
- Lint
- 自定義Lint
- Lint使用
- 優化案
- Apk體積優化
- Kotlin相關
- 簡答1
- 簡答2
- 三方框架相
- Okhttp3源碼分析
- ButterKnife源碼分析
- Glide4源碼分析
- Retrofit源碼分析
- RxJava源碼分析
- ARouter源碼分析
- LeakCanary源碼分析
- WMRouter源碼分析
- 跨平臺相關
- ReactNative
- Flutter
- Hybrid
- 優質源
- 資訊源
- 組件源
- 推薦