今天我們來討論一下,程序中的錯(cuò)誤處理缨该。
在任何一個(gè)穩(wěn)定的程序中偎行,都會有大量的代碼在處理錯(cuò)誤,有一些業(yè)務(wù)錯(cuò)誤贰拿,我們可以通過主動檢查判斷來規(guī)避蛤袒,可對于一些不能主動判斷的錯(cuò)誤,例如 RuntimeException膨更,我們就需要使用try-catch-finally語句了妙真。
有人說,錯(cuò)誤處理并不難啊荚守,try-catch-finally一把梭珍德,try放功能代碼练般,在catch中捕獲異常、處理異常锈候,finally中寫那些無論是否發(fā)生異常薄料,都要執(zhí)行的代碼,這很簡單啊泵琳。
處理錯(cuò)誤的代碼摄职,確實(shí)并不難寫,可是想把錯(cuò)誤處理寫好获列,也并不是一件容易的事情谷市。
接下來我們就從實(shí)現(xiàn)到 JVM 原理,講清楚 Java 的異常處理击孩。
學(xué)東西迫悠,我還是推薦要帶著問題去探索,提前思考幾個(gè)問題吧:
一個(gè)方法巩梢,異常捕獲塊中及皂,不同的地方的 return 語句,誰會生效且改?
catch 和 finally 中出現(xiàn)異常,會如何處理板驳?
try-catch 是否影響效率又跛?
Java 異常捕獲的原理?
二若治、Java 異常處理
2.1 概述
既然是異常處理慨蓝,肯定是區(qū)分異常發(fā)生和捕獲、處理異常端幼,這也正是組成異常處理的兩大要素礼烈。
在 Java 中,拋出的異称排埽可以分為顯示異常和隱式異常此熬,這種區(qū)分主要來自拋出異常的主體是什么,顯示和隱式也是站在應(yīng)用程序的視角來區(qū)分的滑进。
顯示異常的主體是當(dāng)前我們的應(yīng)用程序犀忱,它指的是在應(yīng)用程序中使用 “throw” 關(guān)鍵字,主動將異常實(shí)例拋出扶关。而隱式異常就不受我們控制阴汇, 它觸發(fā)的主體是 Java 虛擬機(jī),指的是 Java 虛擬機(jī)在執(zhí)行過程中节槐,遇到了無法繼續(xù)執(zhí)行的異常狀態(tài)搀庶,續(xù)而將異常拋出拐纱。
對于隱式異常,在觸發(fā)時(shí)哥倔,需要顯示捕獲(try-catch)秸架,或者在方法頭上,用 "throw" 關(guān)鍵字聲明未斑,交由調(diào)用者捕獲處理咕宿。
2.2 使用異常捕獲
在我們編寫異常處理代碼的時(shí)候,主要就是使用前面介紹到的try-catch-finally這三種代碼塊蜡秽。
try 代碼塊:包含待監(jiān)控異常的代碼府阀。
catch 代碼塊:緊跟 try 塊之后,可以指定異常類型芽突。允許指定捕獲多種不同的異常试浙,catch 塊用來捕獲在 try 塊中出發(fā)的某個(gè)指定類型的異常。
finally 代碼塊:緊跟 try 塊或 catch 塊之后寞蚌,用來聲明一段必定會運(yùn)行的代碼田巴。例如用來清理一些資源。
catch 允許存在多個(gè)挟秤,用于針對不同的異常做不同的處理壹哺。如果使用 catch 捕獲多種異常,各個(gè) catch 塊是互斥的艘刚,和 switch 語句類似管宵,優(yōu)先級是從上到下,只能選擇其一去處理異常攀甚。
既然 try-catch-finally 存在多種情況箩朴,并且在發(fā)生異常和不發(fā)生異常時(shí),表現(xiàn)是不一致的秋度,我們就分清楚來單獨(dú)分析炸庞。
1.try塊中,未發(fā)生異常
不觸發(fā)異常荚斯,當(dāng)然是我們樂于看見的埠居。在這種情況下,如果有 finally 塊鲸拥,它會在 try 塊之后運(yùn)行拐格,catch 塊永遠(yuǎn)也不會被運(yùn)行。
2.try塊中刑赶,發(fā)生異常
在發(fā)生異常時(shí)捏浊,會首先檢查異常類型,是否存在于我們的 catch 塊中指定的待捕獲異常撞叨。如果存在金踪,則這個(gè)異常被捕獲浊洞,對應(yīng)的 catch 塊代碼則開始運(yùn)行,finally 塊代碼緊隨其后胡岔。
例如:我們只監(jiān)聽了空指針(NullPointerException)法希,此時(shí)如果發(fā)生了除數(shù)為 0 的崩潰(ArithmeticException),則是不會被處理的靶瘸。
當(dāng)觸發(fā)了我們未捕獲的異常時(shí)苫亦,finally 代碼依然會被執(zhí)行,在執(zhí)行完畢后怨咪,繼續(xù)將異澄萁#“拋出去”。
3.catch 或者 finally 發(fā)生異常
catch 代碼塊和 finally 代碼塊诗眨,也是我們編寫的唉匾,理論上也是有出錯(cuò)的可能。
那么這兩段代碼發(fā)生異常匠楚,會出現(xiàn)什么情況呢巍膘?
當(dāng)在 catch 代碼塊中發(fā)生異常時(shí),此時(shí)的表現(xiàn)取決于 finally 代碼塊中是否存在 return 語句芋簿。如果存在峡懈,則 finally 代碼塊的代碼執(zhí)行完畢直接返回,否則會在 finally 代碼塊執(zhí)行完畢后与斤,將 catch 代碼中新產(chǎn)生的異常逮诲,向外拋出去。
而在極端情況下幽告,finally 代碼塊發(fā)生了異常,則此時(shí)會中斷 finally 代碼塊的執(zhí)行裆甩,直接將異常向外拋出冗锁。
2.3 異常捕獲的返回值
再回頭看看第一個(gè)問題,假如我們寫了一個(gè)方法嗤栓,其中的代碼被try-catch-finally包裹住進(jìn)行異常處理冻河,此時(shí)如果我們在多個(gè)地方都有 return 語句,最終誰的會被執(zhí)行茉帅?
如上圖所示叨叙,在完整的try-catch-finally語句中,finally 都是最后執(zhí)行的堪澎,假設(shè) finally 代碼塊中存在 return 語句擂错,則直接返回,它是優(yōu)先級最高的樱蛤。
一般我們不建議在 finally 代碼塊中添加 return 語句钮呀,因?yàn)檫@會破壞并阻止異常的拋出剑鞍,導(dǎo)致不宜排查的崩潰。
2.4 異常的類型
在 Java 中爽醋,所有的異常蚁署,其實(shí)都是一個(gè)個(gè)異常類,它們都是 Throwable 類或其子類的實(shí)例蚂四。
Throwable 有兩大子類光戈,Exception和Error。
Exception:表示程序可能需要捕獲并且處理的異常遂赠。
Error:表示當(dāng)觸發(fā) Error 時(shí)久妆,它的執(zhí)行狀態(tài)已經(jīng)無法恢復(fù)了,需要中止線程甚至是中止虛擬機(jī)解愤。這是不應(yīng)該被我們應(yīng)用程序所捕獲的異常镇饺。
通常,我們只需要捕獲 Exception 就可以了送讲。但 Exception 中奸笤,有一個(gè)特殊的子類 RuntimeException,即運(yùn)行時(shí)錯(cuò)誤哼鬓,它是在程序運(yùn)行時(shí)监右,動態(tài)出現(xiàn)的一些異常。比較常見的就是 NullPointerException异希、ArrayIndexOutOfBoundsException 等健盒。
Error 和 RuntimeException 都屬于非檢查異常(Unchecked Exception),與之相對的就是普通 Exception 這種屬于檢查異常(Checked Exception)称簿。
所有檢查異常都需要在程序中扣癣,用代碼顯式捕獲,或者在方法中用 throw 關(guān)鍵字顯式標(biāo)注憨降。其實(shí)意思很明顯父虑,要不你自己處理了,要不你拋出去讓別人處理授药。
這種檢查異常的機(jī)制士嚎,是在編譯期間進(jìn)行檢查的,所以如果不按此規(guī)范處理悔叽,在編譯器編譯代碼時(shí)莱衩,就會拋出異常。
2.5 異常處理的性能問題
對于異常處理的性能問題娇澎,其實(shí)是一個(gè)很有爭議的問題笨蚁,有人覺得異常處理是多做了一些工作,肯定對性能是有影響的。但是也有人覺得異常處理的影響赚窃,和增加一個(gè)if-else屬于同種量級册招,對性能的影響其實(shí)微乎其微,是在可以接受的范圍內(nèi)的勒极。
既然有爭議是掰,最簡單的辦法是寫個(gè) Demo 驗(yàn)證一下。當(dāng)然辱匿,我們這里是需要區(qū)分不同的情況键痛,然后根據(jù)解決對比的。
一個(gè)最簡單的 for 循環(huán) 100w 次匾七,在其中做一個(gè)a++的自增操作絮短。
A:無任何try-catch語句。
B:將a++包在try代碼塊中昨忆。
C:在try代碼塊中丁频,觸發(fā)一個(gè)異常。
就是一個(gè)簡單的 for 循環(huán)邑贴,就不貼代碼了席里,異常通過5/0這樣的運(yùn)算,觸發(fā)除數(shù)為 0 的 ArithmeticException 異常拢驾,并在 JDK 1.8 的環(huán)境下運(yùn)行奖磁。
為了避免影響采樣結(jié)果,每個(gè)例子都單獨(dú)運(yùn)行 10 遍之后繁疤,取平均值(單位納秒)
到這里基本上就可以得出結(jié)論了咖为,在沒有發(fā)生異常的情況下,try-catch 對性能的影響微乎其微稠腊。但是一旦發(fā)生異常躁染,性能上則是災(zāi)難性的。
因此架忌,我們應(yīng)該盡可能的避免通過異常來處理正常的邏輯檢查褐啡,這樣可以確保不會因?yàn)榘l(fā)生異常而導(dǎo)致性能問題。
至于為什么發(fā)生異常時(shí)鳖昌,性能差別會有如此之大,就需要從? Java 虛擬機(jī) JVM 的角度來分析了低飒,后面會詳細(xì)分析许昨。
2.6 異常處理無法覆蓋異步回調(diào)
try-catch-finally確實(shí)很好用,但是它并不能捕獲褥赊,異步回調(diào)中的異常糕档。try 語句里的方法,如果允許在另外一個(gè)線程中,其中拋出的異常速那,是無法在調(diào)用者這個(gè)線程中捕獲的俐银。
這一點(diǎn)在使用的過程中,需要特別注意端仰。
三捶惜、JVM 如何處理異常
3.1 JVM 異常處理概述
接下來我們從 JVM 的角度,分析 JVM 如何處理異常荔烧。
當(dāng)異常發(fā)生時(shí)吱七,異常實(shí)例的構(gòu)建,是非常消耗性能的鹤竭。這是由于在構(gòu)造異常實(shí)例時(shí)踊餐,Java 虛擬機(jī)需要生成該異常的異常棧(stack trace)。
異常棧會逐一訪問當(dāng)前線程的 Java 棧幀臀稚,以及各種調(diào)試信息吝岭。包括棧幀所指向的方法名,方法所在的類名吧寺、文件名以及在代碼中是第幾行觸發(fā)的異常窜管。
這些異常輸出到 Log 中,就是我們熟悉的崩潰日志(崩潰棧)撮执。
3.2 崩潰實(shí)例分析異常處理
當(dāng)把 Java 代碼編譯成字節(jié)碼后微峰,每個(gè)方法都會附帶一個(gè)異常表,其中記錄了當(dāng)前方法的異常處理抒钱。
下面直接舉個(gè)例子蜓肆,寫一個(gè)最簡單的try-catch類。
使用?javap -c?進(jìn)行反編譯成字節(jié)碼谋币。
可以看到仗扬,末尾的Exceptions Table就是異常表。異常表中的每一條記錄蕾额,都代表了一個(gè)異常處理器早芭。
異常處理器中,標(biāo)記了當(dāng)前異常監(jiān)控的起始诅蝶、結(jié)束代碼索引退个,和異常處理器的索引。其中 from 指針和 to 指針標(biāo)識了該異常處理器所監(jiān)控的代碼范圍调炬,target 指針則指向異常處理器的起始位置语盈,type 則為最后監(jiān)聽的異常。
例如上面的例子中缰泡,main 函數(shù)中存在異常表刀荒,Exception 的異常監(jiān)聽代碼范圍分別是 [0,8)(不包括 8),異常處理器的索引為 11。
繼續(xù)分析異常處理流程缠借,還需要區(qū)分是否命中異常干毅。
1.命中異常
當(dāng)程序發(fā)生異常時(shí),Java 虛擬機(jī)會從上到下遍歷異常表中所有的記錄泼返。當(dāng)發(fā)現(xiàn)觸發(fā)異常的字節(jié)碼的索引值硝逢,在某個(gè)異常表中某個(gè)異常監(jiān)控的范圍內(nèi)。Java 虛擬機(jī)會判斷所拋出的異常和該條異常監(jiān)聽的異常類型符隙,是否匹配趴捅。如果能匹配上,Java 虛擬機(jī)會將控制流轉(zhuǎn)向至該此異常處理器的 target 索引指向的字節(jié)碼霹疫,這是命中異常的情況拱绑。
2.未命中異常
而如果遍歷完異常表中所有的異常處理器之后,仍未匹配到異常處理器丽蝎,那么它會彈出當(dāng)前方法對應(yīng)的 Java 棧幀猎拨。回到它的調(diào)用者屠阻,在其中重復(fù)此過程红省。
最壞的情況下浓恳,Java 虛擬機(jī)需要遍歷當(dāng)前線程 Java 棧上所有方法的異常表亭引。
3.3 編譯后的 finally 代碼塊
我們寫的代碼,其實(shí)終歸是給人讀的翠霍,但是編譯器干的事兒麻诀,都不是人事兒痕寓。它會把代碼做一些特殊的處理,只是為了讓自己更好解析和執(zhí)行蝇闭。
編譯器對 finally 代碼塊呻率,就是這樣處理的。在當(dāng)前版本的 Java 編譯器中呻引,會將 finally 代碼塊的內(nèi)容礼仗,復(fù)制幾份,分別放在所有可能執(zhí)行的代碼路徑的出口中逻悠。
寫個(gè) Demo 驗(yàn)證一下元践,代碼如下。
繼續(xù)?javap -c?反編譯成字節(jié)碼童谒。
這個(gè)例子中单旁,為了更清晰的看到 finally 代碼塊,我在其中輸出的一段 Log “run finally”惠啄。可以看到,編譯結(jié)果中撵渡,包含了三份 finally 代碼塊融柬。
其中,前兩份分別位于 try 代碼塊和 catch 代碼塊的正常執(zhí)行路徑出口趋距。最后一份則作為全局的異常處理器粒氧,監(jiān)控 try 代碼塊以及 catch 代碼塊。它將捕獲 try 代碼塊觸發(fā)并且未命中 catch 代碼塊捕獲的異常节腐,以及在 catch 代碼塊觸發(fā)的異常外盯。
而 finally 的代碼,如果出現(xiàn)異常翼雀,就不是當(dāng)前方法所能處理的了饱苟,會直接向外拋出。
3.4 異常表中的 any 是什么狼渊?
從上圖中可以看到箱熬,在異常表中,還存在兩個(gè) any 的信息狈邑。
第一個(gè)信息的 from 和 to 的范圍就是 try 代碼塊城须,等于是對 catch 遺漏異常的一種補(bǔ)充,表示會處理所有種類的異常米苹。
第二個(gè)信息的 from 和 to 的范圍糕伐,仔細(xì)看能看到它其實(shí)是 catch 代碼塊,這也正好印證了我們上面的結(jié)論蘸嘶,catch 代碼塊其實(shí)也被異常處理器監(jiān)控著良瞧。
只是如果命中了 any 之后,因?yàn)闆]有對應(yīng)的異常處理器亏较,會繼續(xù)向上拋出去莺褒,交由該方法的調(diào)用方法處理。
四雪情、總結(jié)
到這里我們就基本上講清楚了 Java 異常處理的所有內(nèi)容遵岩。
在日常開發(fā)當(dāng)中,應(yīng)該盡量避免使用異常處理的機(jī)制來處理業(yè)務(wù)邏輯巡通,例如很多代碼中尘执,類型轉(zhuǎn)換就使用try-catch來處理,其實(shí)是很不可取的宴凉。
異常捕獲對應(yīng)用程序的性能確實(shí)有影響誊锭,但也是分情況的。
一旦異常被拋出來弥锄,方法也就跟著 return 了丧靡,捕獲異常棧時(shí)會導(dǎo)致性能變得很慢蟆沫,尤其是調(diào)用棧比較深的時(shí)候。
但是從另一個(gè)角度來說温治,異常拋出時(shí)饭庞,基本上表明程序的錯(cuò)誤。應(yīng)用程序在大多數(shù)情況下熬荆,應(yīng)該是在沒有異常情況的環(huán)境下運(yùn)行的舟山。所以,異常情況應(yīng)該是少數(shù)情況卤恳,只要我們不濫用異常處理累盗,基本上不會影響正常處理的性能問題。
在此我向大家推薦一個(gè)架構(gòu)學(xué)習(xí)交流群突琳。交流學(xué)習(xí)群號:938837867 暗號:555 里面會分享一些資深架構(gòu)師錄制的視頻錄像:有Spring若债,MyBatis,Netty源碼分析本今,高并發(fā)拆座、高性能、分布式冠息、微服務(wù)架構(gòu)的原理挪凑,JVM性能優(yōu)化、分布式架構(gòu)等這些成為架構(gòu)師必備