异常处理背后的JVM原理
1. 异常分类
Java体系中异常的组织分类如下图所示,所有异常类型的根类为 Throwable,具体包括两大类:Error 与 Exception。Error是指程序无法处理的错误,表示运行应用程序中较严重的问题;Exception是指程序本身可以处理的错误,具体可分为运行时异常(派生于RuntimeException)和其他异常。
派生于RuntimeException的异常包含下面几种情况:
- 错误的类型转换
- 数组访问越界
- 访问null指针
“如果出现了RuntimeException异常,那一定是你的问题”,例如访问数组前一定要先检测数组下标是否越界;在使用变量之前检测是否为null来杜绝NullPointerException异常的发生。
Error和RuntimeException是不受检查的异常,其他的所有异常都是受检查的异常。
2. 异常处理在JVM中的体现
public class Demo {
public static void main(String[] args) {
int i=0;
try {
i=10;
} catch (Exception e) {
//TODO: handle exception
i=20;
}
}
}
字节码反编译后
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 12
8: astore_2
9: bipush 20
11: istore_1
12: return
Exception table:
from to target type
2 5 8 Class java/lang/Exception
LineNumberTable:
line 6: 0
line 8: 2
line 12: 5
line 9: 8
line 11: 9
line 13: 12
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 8
locals = [ class "[Ljava/lang/String;", int ]
stack = [ class java/lang/Exception ]
frame_type = 3 /* same */
Exception table中的一列表示try语句的范围,即2-4行(不包括第五行),如果发生的异常与Exception一致,则进入第8行。第8行的astore_2的含义是将异常对象的引用地址储存在e槽位上。
public class Demo {
public static void main(String[] args) {
int i=0;
try {
i=10;
} catch (Exception e) {
i=20;
}finally{
i=30;
}
}
}
字节码反编译后
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: bipush 30
7: istore_1
8: goto 27
11: astore_2
12: bipush 20
14: istore_1
15: bipush 30
17: istore_1
18: goto 27
21: astore_3
22: bipush 30
24: istore_1
25: aload_3
26: athrow
27: return
Exception table:
from to target type
2 5 11 Class java/lang/Exception
2 5 21 any
11 15 21 any
LocalVariableTable:
Start Length Slot Name Signature
12 3 2 e Ljava/lang/Exception;
0 28 0 args [Ljava/lang/String;
2 26 1 i I
finally中的代码被复制了三份,分别放入try流程,catch流程以及catch剩余异常类型流程
3. finally的执行时机
1. finally子句应该是在控制转移语句之前执行
public class Test {
public static void main(String[] args) {
System.out.println("return value of getValue(): " + getValue());
}
public static int getValue() {
try {
return 0;
} finally {
return 1;
}
}
}
/* Output:
return value of getValue(): 1
*///:~
try块被执行,其finally子句一定会被执行,因此在return之前,会执行finally块中的return 1指令,自然后返回1;
2. finally对返回值影响
public class Test{
public static void main(String[] args){
int result=test();
System.out.println(result);
}
public static int test(){
int i=10;
try{
return i;
}finally{
i=20;
}
}
}
// Output: print 10;
虽然在返回i之前,会首先执行finally子句块中的i=20这一步,但是在执行finally这一步之前,会将数据保存在本地变量表中,执行完finally块后,再将其从变量表中取出并返回。因此,尽管finally中改变了i的值,但是返回的是i的一个拷贝