摘要:允許存在多個(gè),用于針對(duì)不同的異常做不同的處理。表示程序可能需要捕獲并且處理的異常。因此,我們應(yīng)該盡可能的避免通過異常來處理正常的邏輯檢查,這樣可以確保不會(huì)因?yàn)榘l(fā)生異常而導(dǎo)致性能問題。異常表中的每一條記錄,都代表了一個(gè)異常處理器。
前言
今天我們來討論一下,程序中的錯(cuò)誤處理。
在任何一個(gè)穩(wěn)定的程序中,都會(huì)有大量的代碼在處理錯(cuò)誤,有一些業(yè)務(wù)錯(cuò)誤,我們可以通過主動(dòng)檢查判斷來規(guī)避,可對(duì)于一些不能主動(dòng)判斷的錯(cuò)誤,例如 RuntimeException,我們就需要使用 try-catch-finally 語句了。
有人說,錯(cuò)誤處理并不難啊,try-catch-finally 一把梭,try 放功能代碼,在 catch 中捕獲異常、處理異常,finally 中寫那些無論是否發(fā)生異常,都要執(zhí)行的代碼,這很簡(jiǎn)單啊。
處理錯(cuò)誤的代碼,確實(shí)并不難寫,可是想把錯(cuò)誤處理寫好,也并不是一件容易的事情。
接下來我們就從實(shí)現(xiàn)到 JVM 原理,講清楚 Java 的異常處理。
學(xué)東西,我還是推薦要帶著問題去探索,提前思考幾個(gè)問題吧:
一個(gè)方法,異常捕獲塊中,不同的地方的 return 語句,誰會(huì)生效?
catch 和 finally 中出現(xiàn)異常,會(huì)如何處理?
try-catch 是否影響效率?
Java 異常捕獲的原理?
二、Java 異常處理 2.1 概述既然是異常處理,肯定是區(qū)分異常發(fā)生和捕獲、處理異常,這也正是組成異常處理的兩大要素。
在 Java 中,拋出的異常可以分為顯示異常和隱式異常,這種區(qū)分主要來自拋出異常的主體是什么,顯示和隱式也是站在應(yīng)用程序的視角來區(qū)分的。
顯示異常的主體是當(dāng)前我們的應(yīng)用程序,它指的是在應(yīng)用程序中使用 “throw” 關(guān)鍵字,主動(dòng)將異常實(shí)例拋出。而隱式異常就不受我們控制, 它觸發(fā)的主體是 Java 虛擬機(jī),指的是 Java 虛擬機(jī)在執(zhí)行過程中,遇到了無法繼續(xù)執(zhí)行的異常狀態(tài),續(xù)而將異常拋出。
對(duì)于隱式異常,在觸發(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 塊之后,用來聲明一段必定會(huì)運(yùn)行的代碼。例如用來清理一些資源。
catch 允許存在多個(gè),用于針對(duì)不同的異常做不同的處理。如果使用 catch 捕獲多種異常,各個(gè) catch 塊是互斥的,和 switch 語句類似,優(yōu)先級(jí)是從上到下,只能選擇其一去處理異常。
既然 try-catch-finally 存在多種情況,并且在發(fā)生異常和不發(fā)生異常時(shí),表現(xiàn)是不一致的,我們就分清楚來多帶帶分析。
1. try塊中,未發(fā)生異常
不觸發(fā)異常,當(dāng)然是我們樂于看見的。在這種情況下,如果有 finally 塊,它會(huì)在 try 塊之后運(yùn)行,catch 塊永遠(yuǎn)也不會(huì)被運(yùn)行。
2. try塊中,發(fā)生異常
在發(fā)生異常時(shí),會(huì)首先檢查異常類型,是否存在于我們的 catch 塊中指定的待捕獲異常。如果存在,則這個(gè)異常被捕獲,對(duì)應(yīng)的 catch 塊代碼則開始運(yùn)行,finally 塊代碼緊隨其后。
例如:我們只監(jiān)聽了空指針(NullPointerException),此時(shí)如果發(fā)生了除數(shù)為 0 的崩潰(ArithmeticException),則是不會(huì)被處理的。
當(dāng)觸發(fā)了我們未捕獲的異常時(shí),finally 代碼依然會(huì)被執(zhí)行,在執(zhí)行完畢后,繼續(xù)將異?!皰伋鋈ァ薄?/p>
3. catch 或者 finally 發(fā)生異常
catch 代碼塊和 finally 代碼塊,也是我們編寫的,理論上也是有出錯(cuò)的可能。
那么這兩段代碼發(fā)生異常,會(huì)出現(xiàn)什么情況呢?
當(dāng)在 catch 代碼塊中發(fā)生異常時(shí),此時(shí)的表現(xiàn)取決于 finally 代碼塊中是否存在 return 語句。如果存在,則 finally 代碼塊的代碼執(zhí)行完畢直接返回,否則會(huì)在 finally 代碼塊執(zhí)行完畢后,將 catch 代碼中新產(chǎn)生的異常,向外拋出去。
而在極端情況下,finally 代碼塊發(fā)生了異常,則此時(shí)會(huì)中斷 finally 代碼塊的執(zhí)行,直接將異常向外拋出。
2.3 異常捕獲的返回值再回頭看看第一個(gè)問題,假如我們寫了一個(gè)方法,其中的代碼被 try-catch-finally 包裹住進(jìn)行異常處理,此時(shí)如果我們?cè)诙鄠€(gè)地方都有 return 語句,最終誰的會(huì)被執(zhí)行?
如上圖所示,在完整的 try-catch-finally 語句中,finally 都是最后執(zhí)行的,假設(shè) finally 代碼塊中存在 return 語句,則直接返回,它是優(yōu)先級(jí)最高的。
一般我們不建議在 finally 代碼塊中添加 return 語句,因?yàn)檫@會(huì)破壞并阻止異常的拋出,導(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í),動(dòng)態(tài)出現(xiàn)的一些異常。比較常見的就是 NullPointerException、ArrayIndexOutOfBoundsException 等。
Error 和 RuntimeException 都屬于非檢查異常(Unchecked Exception),與之相對(duì)的就是普通 Exception 這種屬于檢查異常(Checked Exception)。
所有檢查異常都需要在程序中,用代碼顯式捕獲,或者在方法中用 throw 關(guān)鍵字顯式標(biāo)注。其實(shí)意思很明顯,要不你自己處理了,要不你拋出去讓別人處理。
這種檢查異常的機(jī)制,是在編譯期間進(jìn)行檢查的,所以如果不按此規(guī)范處理,在編譯器編譯代碼時(shí),就會(huì)拋出異常。
2.5 異常處理的性能問題對(duì)于異常處理的性能問題,其實(shí)是一個(gè)很有爭(zhēng)議的問題,有人覺得異常處理是多做了一些工作,肯定對(duì)性能是有影響的。但是也有人覺得異常處理的影響,和增加一個(gè) if-else 屬于同種量級(jí),對(duì)性能的影響其實(shí)微乎其微,是在可以接受的范圍內(nèi)的。
既然有爭(zhēng)議,最簡(jiǎn)單的辦法是寫個(gè) Demo 驗(yàn)證一下。當(dāng)然,我們這里是需要區(qū)分不同的情況,然后根據(jù)解決對(duì)比的。
一個(gè)最簡(jiǎn)單的 for 循環(huán) 100w 次,在其中做一個(gè) a++ 的自增操作。
A:無任何 try-catch 語句。
B:將 a++ 包在 try 代碼塊中。
C:在 try 代碼塊中,觸發(fā)一個(gè)異常。
就是一個(gè)簡(jiǎn)單的 for 循環(huán),就不貼代碼了,異常通過 5/0 這樣的運(yùn)算,觸發(fā)除數(shù)為 0 的 ArithmeticException 異常,并在 JDK 1.8 的環(huán)境下運(yùn)行。
為了避免影響采樣結(jié)果,每個(gè)例子都多帶帶運(yùn)行 10 遍之后,取平均值(單位納秒)。
到這里基本上就可以得出結(jié)論了,在沒有發(fā)生異常的情況下,try-catch 對(duì)性能的影響微乎其微。但是一旦發(fā)生異常,性能上則是災(zāi)難性的。
因此,我們應(yīng)該盡可能的避免通過異常來處理正常的邏輯檢查,這樣可以確保不會(huì)因?yàn)榘l(fā)生異常而導(dǎo)致性能問題。
至于為什么發(fā)生異常時(shí),性能差別會(huì)有如此之大,就需要從 Java 虛擬機(jī) JVM 的角度來分析了,后面會(huì)詳細(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)。
異常棧會(huì)逐一訪問當(dāng)前線程的 Java 棧幀,以及各種調(diào)試信息。包括棧幀所指向的方法名,方法所在的類名、文件名以及在代碼中是第幾行觸發(fā)的異常。
這些異常輸出到 Log 中,就是我們熟悉的崩潰日志(崩潰棧)。
3.2 崩潰實(shí)例分析異常處理當(dāng)把 Java 代碼編譯成字節(jié)碼后,每個(gè)方法都會(huì)附帶一個(gè)異常表,其中記錄了當(dāng)前方法的異常處理。
下面直接舉個(gè)例子,寫一個(gè)最簡(jiǎn)單的 try-catch 類。
使用 javap -c 進(jìn)行反編譯成字節(jié)碼。
可以看到,末尾的 Exceptions Table 就是異常表。異常表中的每一條記錄,都代表了一個(gè)異常處理器。
異常處理器中,標(biāo)記了當(dāng)前異常監(jiān)控的起始、結(jié)束代碼索引,和異常處理器的索引。其中 from 指針和 to 指針標(biāo)識(shí)了該異常處理器所監(jiān)控的代碼范圍,target 指針則指向異常處理器的起始位置,type 則為最后監(jiān)聽的異常。
例如上面的例子中,main 函數(shù)中存在異常表,Exception 的異常監(jiān)聽代碼范圍分別是 [0,8)(不包括 8),異常處理器的索引為 11。
繼續(xù)分析異常處理流程,還需要區(qū)分是否命中異常。
1. 命中異常
當(dāng)程序發(fā)生異常時(shí),Java 虛擬機(jī)會(huì)從上到下遍歷異常表中所有的記錄。當(dāng)發(fā)現(xiàn)觸發(fā)異常的字節(jié)碼的索引值,在某個(gè)異常表中某個(gè)異常監(jiān)控的范圍內(nèi)。Java 虛擬機(jī)會(huì)判斷所拋出的異常和該條異常監(jiān)聽的異常類型,是否匹配。如果能匹配上,Java 虛擬機(jī)會(huì)將控制流轉(zhuǎn)向至該此異常處理器的 target 索引指向的字節(jié)碼,這是命中異常的情況。
2. 未命中異常
而如果遍歷完異常表中所有的異常處理器之后,仍未匹配到異常處理器,那么它會(huì)彈出當(dāng)前方法對(duì)應(yīng)的 Java 棧幀?;氐剿恼{(diào)用者,在其中重復(fù)此過程。
最壞的情況下,Java 虛擬機(jī)需要遍歷當(dāng)前線程 Java 棧上所有方法的異常表。
3.3 編譯后的 finally 代碼塊我們寫的代碼,其實(shí)終歸是給人讀的,但是編譯器干的事兒,都不是人事兒。它會(huì)把代碼做一些特殊的處理,只是為了讓自己更好解析和執(zhí)行。
編譯器對(duì) finally 代碼塊,就是這樣處理的。在當(dāng)前版本的 Java 編譯器中,會(huì)將 finally 代碼塊的內(nèi)容,復(fù)制幾份,分別放在所有可能執(zhí)行的代碼路徑的出口中。
寫個(gè) Demo 驗(yàn)證一下,代碼如下。
繼續(xù) javap -c 反編譯成字節(jié)碼。
這個(gè)例子中,為了更清晰的看到 finally 代碼塊,我在其中輸出的一段 Log “run finally”??梢钥吹剑幾g結(jié)果中,包含了三份 finally 代碼塊。
其中,前兩份分別位于 try 代碼塊和 catch 代碼塊的正常執(zhí)行路徑出口。最后一份則作為全局的異常處理器,監(jiān)控 try 代碼塊以及 catch 代碼塊。它將捕獲 try 代碼塊觸發(fā)并且未命中 catch 代碼塊捕獲的異常,以及在 catch 代碼塊觸發(fā)的異常。
而 finally 的代碼,如果出現(xiàn)異常,就不是當(dāng)前方法所能處理的了,會(huì)直接向外拋出。
3.4 異常表中的 any 是什么?從上圖中可以看到,在異常表中,還存在兩個(gè) any 的信息。
第一個(gè)信息的 from 和 to 的范圍就是 try 代碼塊,等于是對(duì) catch 遺漏異常的一種補(bǔ)充,表示會(huì)處理所有種類的異常。
第二個(gè)信息的 from 和 to 的范圍,仔細(xì)看能看到它其實(shí)是 catch 代碼塊,這也正好印證了我們上面的結(jié)論,catch 代碼塊其實(shí)也被異常處理器監(jiān)控著。
只是如果命中了 any 之后,因?yàn)闆]有對(duì)應(yīng)的異常處理器,會(huì)繼續(xù)向上拋出去,交由該方法的調(diào)用方法處理。
四、總結(jié)到這里我們就基本上講清楚了 Java 異常處理的所有內(nèi)容。
在日常開發(fā)當(dāng)中,應(yīng)該盡量避免使用異常處理的機(jī)制來處理業(yè)務(wù)邏輯,例如很多代碼中,類型轉(zhuǎn)換就使用 try-catch 來處理,其實(shí)是很不可取的。
異常捕獲對(duì)應(yīng)用程序的性能確實(shí)有影響,但也是分情況的。
一旦異常被拋出來,方法也就跟著 return 了,捕獲異常棧時(shí)會(huì)導(dǎo)致性能變得很慢,尤其是調(diào)用棧比較深的時(shí)候。
但是從另一個(gè)角度來說,異常拋出時(shí),基本上表明程序的錯(cuò)誤。應(yīng)用程序在大多數(shù)情況下,應(yīng)該是在沒有異常情況的環(huán)境下運(yùn)行的。所以,異常情況應(yīng)該是少數(shù)情況,只要我們不濫用異常處理,基本上不會(huì)影響正常處理的性能問題。
本文對(duì)你有幫助嗎?留言、點(diǎn)贊、轉(zhuǎn)發(fā)是最大的支持,謝謝!
公眾號(hào)后臺(tái)回復(fù)成長(zhǎng)『成長(zhǎng)』,將會(huì)得到我準(zhǔn)備的學(xué)習(xí)資料,也能回復(fù)『加群』,一起學(xué)習(xí)進(jìn)步;你還能回復(fù)『提問』,向我發(fā)起提問。
推薦閱讀:
“寒冬”正是學(xué)習(xí)時(shí)|關(guān)于字符編碼,你需要知道的都在這里 | 分詞,科普及解決方案| 圖解:HTTP 范圍請(qǐng)求 | 小程序?qū)W習(xí)資料 |HTTP 內(nèi)容編碼 | 輔助模式實(shí)戰(zhàn) | 輔助模式玩出花樣 | 小程序 Flex 布局
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/73057.html
摘要:不受檢查異常為編譯器不要求強(qiáng)制處理的異常,檢查異常則是編譯器要求必須處置的異常。潛在的異常處理器是異常發(fā)生時(shí)依次存留在調(diào)用棧中的方法的集合。當(dāng)運(yùn)行時(shí)系統(tǒng)遍歷調(diào)用棧而未找到合適的異常處理器,則運(yùn)行時(shí)系統(tǒng)終止。異常處理涉及到五個(gè)關(guān)鍵字,分別是。 概念 程序運(yùn)行時(shí),發(fā)生的不被期望的事件,它阻止了程序按照程序員的預(yù)期正常執(zhí)行,這就是異常。 異常是程序中的一些錯(cuò)誤,但并不是所有的錯(cuò)誤都是異常,并...
摘要:異常也就是指程序運(yùn)行時(shí)發(fā)生錯(cuò)誤,而異常處理就是對(duì)這些錯(cuò)誤進(jìn)行處理和控制。有兩個(gè)重要的子類異常和錯(cuò)誤,二者都是異常處理的重要子類,各自都包含大量子類。需要注意的是,一旦某個(gè)捕獲到匹配的異常類型,將進(jìn)入異常處理代碼。 1,異?,F(xiàn)象 程序錯(cuò)誤分為三種:1,編譯錯(cuò)誤;2,運(yùn)行時(shí)錯(cuò)誤;3,邏輯錯(cuò)誤。 編譯錯(cuò)誤是因?yàn)槌绦驔]有遵循語法規(guī)則,編譯程序能夠自己發(fā)現(xiàn)并且提示我們錯(cuò)誤的原因和位置,這...
摘要:可以被異常處理機(jī)制使用,是異常處理的核心。非檢測(cè)異常,在編譯時(shí),不會(huì)提示和發(fā)現(xiàn)異常的存在,不強(qiáng)制要求程序員處理這樣的異常。總體來說,語言的異常處理流程,從程序中獲取異常信息。處理運(yùn)行時(shí)異常,采用邏輯合理規(guī)避同時(shí)輔助處理。 目錄 什么是Java異常? 當(dāng)一個(gè)Exception在程序中發(fā)生的時(shí)候,JVM是怎么做的呢? 當(dāng)我們編寫程序的時(shí)候如何對(duì)待可能出現(xiàn)的異常呢? 正文 1. 什么是J...
摘要:下面是異常處理機(jī)制的語法結(jié)構(gòu)業(yè)務(wù)實(shí)現(xiàn)代碼輸入不合法如果執(zhí)行塊里業(yè)務(wù)邏輯代碼時(shí)出現(xiàn)異常,系統(tǒng)自動(dòng)生成一個(gè)異常對(duì)象,該對(duì)象被提交給運(yùn)行時(shí)環(huán)境,這個(gè)過程被稱為拋出異常。 Java的異常機(jī)制主要依賴于try、catch、finally、throw和throws五個(gè)關(guān)鍵字, try關(guān)鍵字后緊跟一個(gè)花括號(hào)括起來的代碼塊(花括號(hào)不可省略),簡(jiǎn)稱try塊,它里面放置可能引發(fā)異常的代碼 catch后對(duì)...
摘要:根據(jù)異常對(duì)象判斷是否存在異常處理。否則,范圍小的異常會(huì)因異常處理完成而無法處理。異常處理中使用作為異常的統(tǒng)一出口。 參考《第一行代碼java》《java程序設(shè)計(jì)教程》java中程序的錯(cuò)誤有語法錯(cuò)誤、語義錯(cuò)誤。如果是語法性錯(cuò)誤,在編譯時(shí)就可以檢查出來并解決。語義錯(cuò)誤是在程序運(yùn)行時(shí)出現(xiàn)的,在編譯時(shí)沒有錯(cuò)誤,但在運(yùn)行時(shí)可能會(huì)出現(xiàn)錯(cuò)誤導(dǎo)致程序退出,這些錯(cuò)誤稱為異常。在沒有異常處理的情況下,也即...
閱讀 2805·2021-10-11 11:08
閱讀 1525·2021-09-30 09:48
閱讀 1083·2021-09-22 15:29
閱讀 1076·2019-08-30 15:54
閱讀 1006·2019-08-29 15:19
閱讀 563·2019-08-29 13:12
閱讀 3196·2019-08-26 13:53
閱讀 998·2019-08-26 13:28