Java中的异常
简单回顾下Java中的异常,主要有两类:
- 检查异常:必须在代码中显式处理的异常,若不处理则无法通过编译,处理手法有两种:采用try…catch或在方法上声明throws该异常来处理。
- 非检查异常:与检查异常相反,不强制要求处理。
在Java中所有异常都是Throwbale或其子类的实例。如下图, 异常主要分为Error和Exception,其中Excption中还有特殊子类即RuntimeExption。RuntimeException和Error为非检查异常,其余异常基本属于检查异常。
JVM处理异常
代码经编译后,可以在字节码中看到每个方法都有一个异常表,其中的每行记录都代表着一个异常处理器,它由4个字段组成: from, to, target, type。[from,to)(左闭右开)指定了该异常处理器的作用域,target指向异常处理器的起始位置,type指定该异常处理器应处理的异常。如下代码编译:
public class ExceptionControlFlowDemo {
public static void main(String[] args) {
int tryInt,catchInt;
try {
tryInt = 1;
} catch (Exception e) {
catchInt = 2;
}
}
}
编译和并查看字节码:
javac ExceptionControlFlowDemo.java
javap -v ExceptionControlFlowDemo > ExceptionControlFlowDemo.javap
vi ExceptionControlFlowDemo.javap
取关键字节码:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=1
0: iconst_1 // 常量1
1: istore_1
2: goto 8
5: astore_3
6: iconst_2
7: istore_2
8: return
Exception table: // 异常表
from to target type
0 2 5 Class java/lang/Exception
可以看到,异常表中指定了一条异常处理器,处理范围是[0, 2),异常处理器初始位置为5,处理的异常类型为Exception,这与上述代码一致,[0, 2)即[try, catch)中的”tryInt = 1“,5即catch代码块起始位置。
当触发异常时,JVM会从上到下遍历异常表每行记录,当抛出的异常和某行记录的type匹配时,则JVM将控制流转到该记录中target所指向的位置。
已知在try…catch处理异常时,我们还可以使用finally,在finally代码块中释放资源。目前根据Java编译器的不同,针对finally代码块的编译都略有不同(本文采用的openJDK(build 1.8.0_272-b10)附带的编译器),当前编译器会针对各个try和catch代码块在异常表中各自生成一条记录,作用域为try和catch代码块内的处理流程,处理的异常类型为Any,指代所有异常。
编译以下代码并查看字节码:
public class ExceptionControlFlowWithFinallyDemo {
private int tryInt, catchInt, finallyInt, methodInt;
private int i = 0;
public void test() {
try {
tryInt = 0;
if (i < 0) {
return;
} else {
empty();
}
} catch (Exception e) {
catchInt = 1;
} finally {
finallyInt = 2;
}
methodInt = 3;
}
public void empty() {}
}
javac ExceptionControlFlowWithFinallyDemo.java
javap -v ExceptionControlFlowWithFinallyDemo > ExceptionControlFlowWithFinallyDemo.javap
vi ExceptionControlFlowWithFinallyDemo.javap
截取关键字节码:
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: iconst_0
2: putfield #3 // Field tryInt:I
5: aload_0
6: getfield #2 // Field i:I
9: ifge 18
12: aload_0
13: iconst_2
14: putfield #4 // Field finallyInt:I
17: return
18: aload_0
19: invokevirtual #5 // Method empty:()V
22: aload_0
23: iconst_2
24: putfield #4 // Field finallyInt:I
27: goto 52
30: astore_1
31: aload_0
32: iconst_1
33: putfield #7 // Field catchInt:I
36: aload_0
37: iconst_2
38: putfield #4 // Field finallyInt:I
41: goto 52
44: astore_2
45: aload_0
46: iconst_2
47: putfield #4 // Field finallyInt:I
50: aload_2
51: athrow
52: aload_0
53: iconst_3
54: putfield #8 // Field methodInt:I
57: return
Exception table:
from to target type
0 12 30 Class java/lang/Exception
18 22 30 Class java/lang/Exception
0 12 44 any
18 22 44 any
30 36 44 any
可以看到异常表中有五行记录,而代码中仅显式捕获并处理了一个异常,这正是Java编译器将finally代码块编译后的字节码复制到了各个try…catch代码块内的处理流程末(在简单的控制流中,java编译器不会复制finally代码块的字节码,仅生成异常表中的记录,target指向finally起始位置)。
从上边的字节码中可以看出,即使采用了finally,当finally代码块中触发异常后即退出了方法栈,仍无法保证余下的代码块能够执行,即无法执行到return。在Java 7之前针对资源的处理仍需要在finally代码块中再做一些判断,如判断是否为null。在Java7之后,引入了try with resources语法,可以安全地关闭(TODO)。
同样可从字节码中看出的,当catch捕获到指定异常,但在处理异常的过程中引发了新的异常,那么finally处理的是新异常,原异常的信息(stack trace)将丢失。
总结
在Java中异常分为检查异常和非检查异常,检查异常需要在代码中显式处理,否则无法通过编译。
针对异常的处理,编译器会生成异常表,异常表由4个字段组成: from, to, target, type。[from,to)(左闭右开)指定了该异常处理器的作用域,target指向异常处理器的起始位置,type指定该异常处理器应处理的异常。
针对finally的处理,编译器会针对每个try和catch代码块在异常表中都生成一条记录,作用域为try和catch代码块内的处理流程,处理的异常类型为Any,指代所有异常。

939

被折叠的 条评论
为什么被折叠?



