寫(xiě)那么多BUG,你知道異常是怎么被拋出來(lái)的嗎?
共 20040字,需瀏覽 41分鐘
·
2024-11-29 18:01
你知道的越多,不知道的就越多,業(yè)余的像一棵小草!
你關(guān)注,我們一起精進(jìn)!你星標(biāo),我們便有了更多故事!
編輯:業(yè)余草
來(lái)源:juejin.cn/post/7442232749606731828
推薦:https://t.zsxq.com/rpgBj
引言
Java 虛擬機(jī)里面的異常使用 Throwable 或其子類的實(shí)例來(lái)表示,拋異常的本質(zhì)實(shí)際上是程序控制權(quán)的一種即時(shí)的、非局部(Nonlocal)的轉(zhuǎn)換——從異常拋出的地方轉(zhuǎn)換至處理異常的地方。絕大多數(shù)的異常的產(chǎn)生都是由于當(dāng)前線程執(zhí)行的某個(gè)操作所導(dǎo)致的,這種可以稱為是同步的異常。與之相對(duì)的,異步異常是指在程序的其他任意地方進(jìn)行的動(dòng)作而導(dǎo)致的異常。
Java 虛擬機(jī)中異常的出現(xiàn)總是由下面三種原因之一導(dǎo)致的:
1、虛擬機(jī)同步檢測(cè)到程序發(fā)生了非正常的執(zhí)行情況,這時(shí)異常將會(huì)緊接著在發(fā)生非正常執(zhí)行情況的字節(jié)碼指令之后拋出。
-
字節(jié)碼指令所蘊(yùn)含的操作違反了 Java 語(yǔ)言的語(yǔ)義,如訪問(wèn)一個(gè)元素。 -
類在加載或者鏈接時(shí)出現(xiàn)錯(cuò)誤。 -
使用某些資源的時(shí)候產(chǎn)生資源限制,例如使用了太多的內(nèi)存
2、athrow 字節(jié)碼指令被執(zhí)行。
3、由于以下原因,導(dǎo)致了異步異常的出現(xiàn):
-
調(diào)用了 Thread 或者 ThreadGroup 的 -
Java 虛擬機(jī)實(shí)現(xiàn)的內(nèi)部程序錯(cuò)誤。
理解異常
Java 異常的底層實(shí)現(xiàn)涉及到編譯器和虛擬機(jī)(JVM)兩個(gè)層面。包括編譯器如何處理異常代碼以及虛擬機(jī)如何在運(yùn)行時(shí)處理異常。
編譯器層面
示例
try {
// 可能引發(fā)異常的代碼
} catch (SomeException e) {
// 處理 SomeException 的代碼
} finally {
// 無(wú)論是否發(fā)生異常都會(huì)執(zhí)行的代碼
}
編譯器處理
編譯器在將源代碼編譯成字節(jié)碼時(shí),會(huì)對(duì)異常相關(guān)的代碼進(jìn)行處理。
-
生成異常表(Exception Table): 編譯器會(huì)生成一個(gè)異常表,其中包含了 try塊的起始和結(jié)束位置,以及每個(gè)catch塊和finally塊的起始位置。這個(gè)表是在字節(jié)碼中的一部分,用于在運(yùn)行時(shí)確定異常處理邏輯。 -
異常處理代碼的插入: 編譯器會(huì)在可能引發(fā)異常的代碼周圍插入異常處理代碼,以確保異常發(fā)生時(shí)能夠跳轉(zhuǎn)到正確的 catch塊或finally塊。
虛擬機(jī)層面
JVM實(shí)現(xiàn)
JVM在運(yùn)行時(shí)負(fù)責(zé)執(zhí)行編譯生成的字節(jié)碼。
-
異常對(duì)象的創(chuàng)建: 當(dāng)在 try塊中的代碼引發(fā)異常時(shí),JVM會(huì)創(chuàng)建一個(gè)異常對(duì)象,其中包含有關(guān)異常的信息,如類型、消息和堆棧跟蹤。 -
異常拋出: JVM使用 athrow指令將異常對(duì)象拋出。這通常由throw關(guān)鍵字觸發(fā)。 -
異常處理表的使用: 當(dāng)異常被拋出時(shí),JVM會(huì)檢查當(dāng)前方法的異常處理表。它會(huì)逐個(gè)檢查 try塊,看是否匹配拋出的異常。如果找到匹配的catch塊,JVM會(huì)跳轉(zhuǎn)到該塊的代碼執(zhí)行異常處理邏輯。 -
finally 塊的執(zhí)行: 無(wú)論是否發(fā)生異常,JVM都會(huì)執(zhí)行 finally塊中的代碼。這是通過(guò)在try塊的最后插入finally指令實(shí)現(xiàn)的。
源碼示例
以下是 try-catch-finally 示例
package com.example.demo.exception;
public class TryCatchFinallyExample {
public static void main(String[] args) {
try {
int result = divide(10, 0);
System.out.println("Result: " + result);
} catch (ArithmeticException e) {
System.out.println("Caught ArithmeticException: " + e.getMessage());
} finally {
System.out.println("Finally block executed");
}
}
public static int divide(int a, int b) {
return a / b;
}
}
對(duì)應(yīng)的字節(jié)碼(使用 javap -c 命令查看):
-
先執(zhí)行編譯命令 javac TryCatchFinallyExample.java -
在執(zhí)行 javap -c TryCatchFinallyExample
Compiled from "TryCatchFinallyExample.java"
public class com.example.demo.exception.TryCatchFinallyExample {
public com.example.demo.exception.TryCatchFinallyExample();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: bipush 10
2: iconst_0
3: invokestatic #2 // Method divide:(II)I
6: istore_1
7: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
10: iload_1
11: invokedynamic #4, 0 // InvokeDynamic #0:makeConcatWithConstants:(I)Ljava/lang/String;
16: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
19: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
22: ldc #6 // String Finally block executed
24: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
27: goto 68
30: astore_1
31: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
34: aload_1
35: invokevirtual #8 // Method java/lang/ArithmeticException.getMessage:()Ljava/lang/String;
38: invokedynamic #9, 0 // InvokeDynamic #1:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
43: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
46: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
49: ldc #6 // String Finally block executed
51: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
54: goto 68
57: astore_2
58: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
61: ldc #6 // String Finally block executed
63: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
66: aload_2
67: athrow
68: return
Exception table:
from to target type
0 19 30 Class java/lang/ArithmeticException
0 19 57 any
30 46 57 any
public static int divide(int, int);
Code:
0: iload_0
1: iload_1
2: idiv
3: ireturn
}
Exception table(異常表)
Exception table 是Java字節(jié)碼中的一個(gè)部分,用于指定方法中的異常處理信息。它描述了在方法執(zhí)行期間,哪些字節(jié)碼范圍可能拋出異常,以及如何處理這些異常。
我們具體解釋 Exception table 部分的含義:
Exception table:
from to target type
0 19 30 Class java/lang/ArithmeticException
0 19 57 any
30 46 57 any
每一行代表一個(gè)異常處理?xiàng)l目,它包含以下信息:
-
from: 起始字節(jié)碼索引,表示異常處理的起始位置。 -
to: 結(jié)束字節(jié)碼索引,表示異常處理的結(jié)束位置。 -
target: 處理異常時(shí)的目標(biāo)字節(jié)碼索引,表示異常被捕獲后應(yīng)該跳轉(zhuǎn)到的位置。 -
type: 異常類型,表示應(yīng)該捕獲的異常類型。
第一行: 如果0到19之間,發(fā)生了ArithmeticException類型的異常,調(diào)用30的位置處理異常。
-
異常處理范圍:從字節(jié)碼索引0到19。 -
異常類型: java/lang/ArithmeticException。 -
處理后跳轉(zhuǎn)到字節(jié)碼索引30。
第二行: 如果0到19之間,發(fā)生了任何類型的異常,調(diào)用57的位置處理異常。
-
異常處理范圍:從字節(jié)碼索引0到19。 -
異常類型: any,表示捕獲任何異常。 -
處理后跳轉(zhuǎn)到字節(jié)碼索引57。
第三行: 如果30到46之間(即catch部分),發(fā)生了任何類型的異常,調(diào)用57的位置處理異常。
-
異常處理范圍:從字節(jié)碼索引30到46。 -
異常類型: any,表示捕獲任何異常。 -
處理后跳轉(zhuǎn)到字節(jié)碼索引57。
通過(guò)這個(gè)異常表的信息,它告訴Java虛擬機(jī)在執(zhí)行方法時(shí),如果在指定的范圍內(nèi)發(fā)生了異常,應(yīng)該如何處理。每個(gè)異常處理?xiàng)l目都包含了異常的類型和處理的范圍。如果異常發(fā)生在范圍內(nèi),程序?qū)凑债惓L幚肀碇兄付ǖ姆绞竭M(jìn)行處理,跳轉(zhuǎn)到相應(yīng)的目標(biāo)位置。
再次分析上面的指令
public static void main(java.lang.String[]);
Code:
// try 獲取 finally 的代碼,如果沒(méi)有異常發(fā)生,則執(zhí)行輸出finally的操作,跳到goto的68位置,執(zhí)行返回操作。
0: bipush 10
2: iconst_0
3: invokestatic #2 // Method divide:(II)I
6: istore_1
7: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
10: iload_1
11: invokedynamic #4, 0 // InvokeDynamic #0:makeConcatWithConstants:(I)Ljava/lang/String;
16: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
19: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
22: ldc #6 // String Finally block executed
24: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
27: goto 68
// catch 獲取 finally代碼,如果沒(méi)有異常發(fā)生,則執(zhí)行輸出finally的操作,跳到goto的68位置,執(zhí)行返回操作。
30: astore_1
31: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
34: aload_1
35: invokevirtual #8 // Method java/lang/ArithmeticException.getMessage:()Ljava/lang/String;
38: invokedynamic #9, 0 // InvokeDynamic #1:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
43: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
46: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
49: ldc #6 // String Finally block executed
51: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
54: goto 68
// finally 的代碼如果被調(diào)用,既有可能是try的異常,也有可能是catch的異常。
57: astore_2
58: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
61: ldc #6 // String Finally block executed
63: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
66: aload_2
// 如果異常沒(méi)有被catch捕獲,到了這里,執(zhí)行完finally的語(yǔ)句后,也要把這個(gè)異常拋出去,傳遞給調(diào)用處。
67: athrow
68: return
關(guān)于指令的解釋
-
bipush 10:將整數(shù)值10推送到操作數(shù)棧上。 -
iconst_0:將整數(shù)值0推送到操作數(shù)棧上。 -
invokestatic #2:**(調(diào)用靜態(tài)方法)**調(diào)用靜態(tài)方法divide,傳入兩個(gè)整數(shù)參數(shù),并接收一個(gè)整數(shù)結(jié)果。 -
istore_1:將操作數(shù)棧頂?shù)恼麛?shù)值存儲(chǔ)到本地變量表的第一個(gè)位置。 -
getstatic #3:獲取System.out字段并將其推送到操作數(shù)棧上。 -
iload_1:將第一個(gè)局部變量(即從divide方法返回的結(jié)果)加載到操作數(shù)棧上。 -
invokedynamic #4, 0:**(調(diào)用動(dòng)態(tài)方法)**動(dòng)態(tài)生成并調(diào)用一個(gè)方法,該方法接受一個(gè)整數(shù)參數(shù),并返回一個(gè)字符串。 -
invokevirtual #5:**(調(diào)用實(shí)例方法)**調(diào)用PrintStream.println方法,打印出字符串。 -
getstatic #3:獲取System.out字段并將其推送到操作數(shù)棧上。 -
ldc #6:**( 將 int, float 或 String 型常量值從常量池中推送至棧頂。)**將常量池中的字符串"Finally block executed"加載到操作數(shù)棧上。 -
invokevirtual #5:調(diào)用PrintStream.println方法,打印出字符串。 -
goto 68:無(wú)條件跳轉(zhuǎn)至第68行。 -
astore_1:將操作數(shù)棧上的值存儲(chǔ)到本地變量表的第一個(gè)位置(發(fā)生異常時(shí),將異常對(duì)象存入這個(gè)位置)。 -
getstatic #3:獲取System.out字段并將其推送到操作數(shù)棧上。 -
aload_1:將第一個(gè)局部變量(即捕獲到的異常對(duì)象)加載到操作數(shù)棧上。 -
invokevirtual #8:調(diào)用ArithmeticException.getMessage方法,獲取異常消息并將其推送到操作數(shù)棧上。 -
invokedynamic #9, 0:動(dòng)態(tài)生成并調(diào)用一個(gè)方法,該方法接受一個(gè)字符串參數(shù),并返回一個(gè)字符串。 -
invokevirtual #5:調(diào)用PrintStream.println方法,打印出字符串。 -
getstatic #3:獲取System.out字段并將其推送到操作數(shù)棧上。 -
ldc #6:將常量池中的字符串"Finally block executed"加載到操作數(shù)棧上。 -
invokevirtual #5:調(diào)用PrintStream.println方法,打印出字符串。 -
goto 68:無(wú)條件跳轉(zhuǎn)至第68行。 -
astore_2:將操作數(shù)棧上的值存儲(chǔ)到本地變量表的第二個(gè)位置(發(fā)生異常時(shí),將新的異常對(duì)象存入這個(gè)位置)。 -
getstatic #3:獲取System.out字段并將其推送到操作數(shù)棧上。 -
ldc #6:將常量池中的字符串"Finally block executed"加載到操作數(shù)棧上。 -
invokevirtual #5:調(diào)用PrintStream.println方法,打印出字符串。 -
aload_2:將第二個(gè)局部變量(即新的異常對(duì)象)加載到操作數(shù)棧上。 -
athrow:將棧頂?shù)漠惓伋觥? -
return:返回void。
關(guān)于指令的操作,大家可以閱讀《Java虛擬機(jī)規(guī)范》- 第 6 章 Java 虛擬機(jī)指令集。
總結(jié)
當(dāng)程序執(zhí)行過(guò)程中發(fā)生異常時(shí),Java虛擬機(jī)(JVM)會(huì)按照以下流程處理異常:
-
執(zhí)行 try : 程序執(zhí)行到 try塊中的字節(jié)碼指令。 -
檢測(cè)異常發(fā)生: 當(dāng)在 try塊中發(fā)生異常時(shí),Java虛擬機(jī)會(huì)檢測(cè)到異常的發(fā)生。 -
異常表匹配: 異常表是在編譯時(shí)生成的,它包含了每個(gè) try-catch塊的起始位置、結(jié)束位置、異常處理器的位置以及期望捕獲的異常類型。異常表將被檢查以查找與發(fā)生的異常類型匹配的處理器。 -
執(zhí)行字節(jié)碼指令: 在 try塊中的字節(jié)碼指令將繼續(xù)執(zhí)行,直到異常發(fā)生。 -
拋出異常: 當(dāng)異常發(fā)生時(shí),Java虛擬機(jī)會(huì)創(chuàng)建一個(gè)異常對(duì)象,并將其拋出。 -
查找匹配的異常處理器: 異常表中的每一項(xiàng)都將被檢查,如果發(fā)生的異常類型匹配,就會(huì)選擇相應(yīng)的異常處理器。 -
遇到異常處理指令: 當(dāng)匹配到異常處理器時(shí),控制流將跳轉(zhuǎn)到異常處理器的起始位置。這可能涉及到 goto指令或其他控制流程的改變。 -
異常表中的處理器執(zhí)行: 執(zhí)行異常處理器( catch塊或finally塊)中的字節(jié)碼指令。在catch塊中,會(huì)進(jìn)行對(duì)異常對(duì)象的處理,而finally塊則無(wú)論是否發(fā)生異常都會(huì)執(zhí)行。 -
執(zhí)行 catch 或 finally: 在異常處理器中執(zhí)行相應(yīng)的字節(jié)碼指令,處理異?;驁?zhí)行清理代碼。 -
控制流繼續(xù)執(zhí)行: 一旦異常處理完成,程序的控制流將繼續(xù)執(zhí)行異常處理代碼塊之后的代碼。
