Oracle官方對異常給出了以下定義:java
Definition: An exception is an event, which occurs during the execution of a program, that disrupts the normal flow of the program's instructions.程序員
簡單翻譯就是一個異常是在程序在執行過程當中出現的事件,它擾亂了正常的指令流(翻譯的很差,見諒)。程序在運行的過程當中會由於各類各樣的因素致使程序沒法繼續執行,例如找不到文件、網絡鏈接超時、解析文件失敗等等,Java將這種致使程序沒法正常執行的因素抽象成「異常」,並以此細分各類各樣的「異常」,再結合「異常處理」構成了整個異常體系,所謂「異常處理」指的就是當程序發生異常的時候,程序能本身處理異常,並嘗試恢復異常,使程序能繼續正常的運行而不須要外界認爲的干預。下面我將逐步深刻的介紹Java異常體系中幾個重要的點,包括但不限於:shell
實際上,異常和異常處理機制在計算機硬件上就有的機制,各類編程語言對其作了抽象,使得異常的檢測、處理更加方便、高效。編程
上圖是Java異常類結構圖,從圖中能夠看到Throwable是整個異常類體系的父類,它有兩個最主要的子類,分別是Error和Exception。網絡
Exception即異常,是應用程序自己能夠處理的,Java將其分爲兩大類:多線程
Error即錯誤,由於Error每每是虛擬機相關的比較嚴重的錯誤,應用程序通常是沒有能力恢復的,例如StackOverflowError(棧溢出)、OutOfMemoryError(內存溢出)等,虛擬機對這種錯誤的處理方法通常是直接中止相關線程(也就是說,若是應用程序是多線程併發程序,那麼即便出現了Error,應用程序也極可能不會直接退出)。實際上,Java雖然沒有禁止應用程序捕獲Error,但咱們也應該儘可能不要去作這事,由於這種錯誤並非程序邏輯錯誤,而是虛擬機發生的錯誤,基本是不可修復的,若是捕獲了但沒法處理的話,咱們將沒法獲得錯誤堆棧,致使難以排查問題。併發
Java中異常處理機制包含三個方面:檢測異常,捕獲異常以及處理異常。編程語言
咱們可使用try關鍵字來指定一個範圍,該範圍就是異常檢測的範圍,而後使用catch建立一個異常處理塊(在Java中,若是隻有try而沒有catch則沒法經過經過編譯)假設有以下代碼:ui
public static void method1() {
try {
method2();
} catch (IOException e) {
System.out.println("catch io exception");
}
}
複製代碼
先用javac將其編譯,而後使用javap -verbose XXX.class 將字節碼信息翻譯並打印出來,結果以下所示:spa
public static void method1() throws java.io.IOException;
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=0
0: invokestatic #6 // Method method2:()V
3: goto 15
6: astore_0
7: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
10: ldc #8 // String catch io exception
12: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
15: return
Exception table: //異常表
from to target type
0 3 6 Class java/io/IOException
LineNumberTable:
line 19: 0
line 22: 3
line 20: 6
line 21: 7
line 23: 15
StackMapTable: number_of_entries = 2
frame_type = 70 /* same_locals_1_stack_item */
stack = [ class java/io/IOException ]
frame_type = 8 /* same */
複製代碼
主要看看 Exception table(即異常表)標籤,發現只有一行數據,有from、to、target、type等字段。from、to即構成了異常檢測的範圍(例子中即0~3),target表明異常處理開始的字節碼索引(例子中即索引爲6的字節碼),type表示異常處理器所處理的異常類型(例子中是IOException)。
如今來看看 Exception table(異常表),異常表裏的一行數據表示一個異常處理器,每行數據有from、to、target、type四個字段,前三個字段的值都是字節碼的索引,type的值是一個符號引用,表明了異常處理器所處理的類型。每一個方法都會有一個異常表,但有時候咱們沒有在javap的打印結果中看到,這是由於對應的方法沒有異常處理器,即異常表中沒有任何數據,javap只是將其省略了而已。
當有異常發生的時候,虛擬機會遍歷異常表,首先檢查出現異常的位置是否在異常表中某個條目的檢測範圍內(from-to字段),若是有這樣的一個條目,將繼續檢查所拋出的異常是不是和type字段描述的異常匹配,若是匹配,就跳轉到target值所指向的字節碼進行異常處理。若是遍歷完整個表也沒有找到匹配的行,那麼就會彈出棧,並在此時的棧幀上繼續執行如上操做,最壞的狀況就是虛擬機須要遍歷整個方法調用棧中全部的異常表,若是最後仍是沒有找到匹配的異常表條目,虛擬機將直接將異常拋出,並打印異常堆棧信息。
上面的文字描述可能會有點繞,不用擔憂,看看下面這張邏輯流程圖,結合文字描述,應該就能夠理解異常處理的流程了。
其實從上面的流程描述中,還隱含了一個重要的知識點:異常傳播機制。即當前方法沒法處理的時候,異常會傳播到調用方,繼續嘗試處理異常,如此往復,知道最頂層的調用方,若是仍是沒有合適的異常處理,那麼就直接中止線程,拋出異常並打印異常堆棧。下面的代碼演示了異常傳播機制:
public class Main {
public static void main(String[] args) {
method1();
System.out.println("continue...");
}
public static void method1() {
try {
method2();
} catch (IOException e) {
System.out.println("catch io exception");
}
}
public static void method2() throws IOException{
method3();
}
public static void method3() throws IOException {
throw new IOException("method3");
}
}
複製代碼
代碼中,main方法調用method1,method1調用method2,method2調用method3,在method3中拋出了一個IOEception,由於IOException是一個受檢異常,因此method2要麼使用try-catch構建一個異常處理器,要麼使用throws關鍵字將異常繼續往上拋,method2選擇的是往上拋出異常,method1則是構建了一個異常處理器,若是該異常處理器能正確的捕獲並處理異常,則不會再往上拋異常了,因此main方法不須要作特殊處理。運行一下,結果大體以下所示:
catch io exception
continue...
複製代碼
發現continue能正確輸出,說明main線程沒有被中止,即異常已經被正確處理了。如今來修改一下代碼,以下所示:
public static void method1() throws IOException {
method2();
}
//其餘部分代碼沒有變化
複製代碼
此時再次運行,結果大體以下:
Exception in thread "main" java.io.IOException: method3
at top.yeonon.exception.Main.method3(Main.java:26)
at top.yeonon.exception.Main.method2(Main.java:22)
at top.yeonon.exception.Main.method1(Main.java:18)
at top.yeonon.exception.Main.main(Main.java:12)
複製代碼
發現打印了異常堆棧,可是沒有打印continue,說明main線程並虛擬機中止了,沒能繼續執行。這是由於在整個方法調用棧中,沒有在任何一個方法的異常表找到匹配的異常表條目,即沒有找到合適的異常處理器,最終沒有辦法了,只能中止線程並拋出異常,期望程序員能處理了。
到如今爲止,我一直沒有提到finally,但其實finally也是一個很重要的組件。finally能夠結合try-catch塊,不管是否發生異常,都會執行finally裏的邏輯。finally的設計初衷是爲了不程序員忘記寫上一些清理操做的代碼,例如關閉網絡鏈接、文件IO鏈接等。
finally代碼塊的編譯也是比較複雜的,編譯器(當前版本的編譯器)並非直接使用跳轉指令來實現「不管是否發生異常都會執行finally」功能的。而是採用「複製」的方法,將finally塊的代碼複製到try-catch塊全部正常執行路徑以及異常執行路徑的出口位置。以下圖所示(圖來自極客時間上關於JVM的一門課程,在最後我會標註):
變種1和變種2的邏輯實際上是同樣的,只是finally塊所在的位置不太同樣而已。如今假設有以下代碼:
public class Main {
public static void main(String[] args) {
try {
method3();
} catch (IOException e) {
System.out.println("catch io exception");
} finally {
System.out.println("execute finally block");
}
System.out.println("continue...");
}
public static void method3() throws IOException {
throw new IOException("method3");
}
}
複製代碼
一樣編譯後,使用javap來輸出可閱讀的字節碼,以下所示:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: invokestatic #2 // Method method3:()V
3: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
6: ldc #4 // String execute finally block
8: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
11: goto 45
14: astore_1
15: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
18: ldc #7 // String catch io exception
20: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
23: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
26: ldc #4 // String execute finally block
28: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
31: goto 45
34: astore_2
35: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
38: ldc #4 // String execute finally block
40: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
43: aload_2
44: athrow
45: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
48: ldc #8 // String continue...
50: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
53: return
Exception table:
from to target type
0 3 14 Class java/io/IOException
0 3 34 any
14 23 34 any
複製代碼
注意一下六、2六、38號指令和其先後兩條指令,發現其實就是finally塊代碼的內容,即輸出 execute finally block字符串。並且剛好有3份,和以前所描述的已知。而後來看看異常表,重點看看後面兩行,這裏比較特殊的就是type字段,該字段的值是any,javap用這個來指代全部異常,即這兩個條目要處理的就是全部異常。其中的第一條form-to的範圍是0~3,發現是try塊的的範圍,第二條from-to的範圍是14~23,發現實際上是catch塊。爲何會這樣呢?
首先說try塊的,若是咱們本身定義的異常處理器沒法和發生的異常匹配,那麼就會被捕獲全部異常的異常處理器捕獲,並跳轉到異常處理器所在的位置,例如這裏的34號指令,咱們發現其實34號指令就是finally塊本來所在的位置,也就是說,即便發現了沒有捕獲到的異常,也會走到finally塊的邏輯中。對於正常的狀況,則是不會走到34號開始的代碼塊的,而是直接goto(11號指令)到45號指令處。
而後就是catch塊,由於在catch塊裏也有可能發生異常的,因此加上這麼一個異常捕獲器,而且和上面的同樣,跳轉到34號指令處執行finally代碼,若是在catch塊裏沒有發生異常,和try塊那裏同樣,繼續執行復制過來的finally塊的代碼,執行完畢後直接goto(31號指令)到45號指令處,也沒有執行最後的從34號開始的finally塊。
這也就是爲何在整個try-catch-finally結構中,不管是否發生異常,老是會執行finally裏的邏輯。
本文簡單介紹了異常的概念、分類以及異常處理機制。尤爲是異常處理機制,咱們深刻到字節碼層面去查看整個處理機制的執行流程,相信你們會對異常處理有更深入的認識。finally也是一個很重要的組件,其做用就是在整個try-catch-finally結構中,不管是否發生異常,都會執行finally塊裏的邏輯,而且我也嘗試深刻到字節碼中分析這個功能是如何實現的。
極客時間: 深刻拆解 Java 虛擬機第6節 :JVM是如何處理異常的?