Java 異常處理中的種種細(xì)節(jié)其屏!

今天我們來討論一下,程序中的錯(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 有兩大子類光戈,ExceptionError

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)師必備

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末逛艰,一起剝皮案震驚了整個(gè)濱河市躏碳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌散怖,老刑警劉巖菇绵,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異镇眷,居然都是意外死亡咬最,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進(jìn)店門欠动,熙熙樓的掌柜王于貴愁眉苦臉地迎上來永乌,“玉大人,你說我怎么就攤上這事具伍〕岢” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵人芽,是天一觀的道長望几。 經(jīng)常有香客問我,道長萤厅,這世上最難降的妖魔是什么橄抹? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任靴迫,我火速辦了婚禮,結(jié)果婚禮上楼誓,老公的妹妹穿的比我還像新娘矢劲。我一直安慰自己,他們只是感情好慌随,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著躺同,像睡著了一般阁猜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蹋艺,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天剃袍,我揣著相機(jī)與錄音,去河邊找鬼捎谨。 笑死民效,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的涛救。 我是一名探鬼主播畏邢,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼检吆!你這毒婦竟也來了舒萎?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤蹭沛,失蹤者是張志新(化名)和其女友劉穎臂寝,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體摊灭,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡咆贬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了帚呼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掏缎。...
    茶點(diǎn)故事閱讀 38,617評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖萝挤,靈堂內(nèi)的尸體忽然破棺而出御毅,到底是詐尸還是另有隱情,我是刑警寧澤怜珍,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布端蛆,位于F島的核電站,受9級特大地震影響酥泛,放射性物質(zhì)發(fā)生泄漏今豆。R本人自食惡果不足惜嫌拣,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望呆躲。 院中可真熱鬧异逐,春花似錦、人聲如沸插掂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽辅甥。三九已至酝润,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間璃弄,已是汗流浹背要销。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留夏块,地道東北人疏咐。 一個(gè)月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像脐供,于是被迫代替她去往敵國和親浑塞。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評論 2 348

推薦閱讀更多精彩內(nèi)容