参考文献

  • 极客时间-JVM是如何处理异常的?

异常处理

  • 异常处理的两大组成要素是抛出异常和捕获异常.这两大要素共同实现程序控制流的非正常转移.
  • 抛出异常可分为显式和隐式两种.显式抛异常的主体是应用程序,它指的是在程序中使用throw关键字,手动将异常实例抛出.
  • 隐式抛异常的主体则是 Java 虚拟机,它指的是 Java 虚拟机在执行过程中,碰到无法继续执行的异常状态,自动抛出异常.举例来说,Java 虚拟机在执行读取数组操作时,发现输入的索引值是负数,故而抛出数组索引越界异常(ArrayIndexOutOfBoundsException).
  • 捕获异常则涉及了如下三种代码块:
    • try 代码块:用来标记需要进行异常监控的代码.
    • catch 代码块:跟在 try 代码块之后,用来捕获在 try 代码块中触发的某种指定类型的异常.除了声明所捕获异常的类型之外,catch 代码块还定义了针对该异常类型的异常处理器.在 Java 中,try 代码块后面可以跟着多个 catch 代码块,来捕获不同类型的异常.Java 虚拟机会从上至下匹配异常处理器.因此,前面的 catch 代码块所捕获的异常类型不能覆盖后边的,否则编译器会报错.
    • finally代码块:跟在 try 代码块和 catch 代码块之后,用来声明一段必定运行的代码.它的设计初衷是为了避免跳过某些关键的清理代码,例如关闭已打开的系统资源.

Java中的异常

  • 在 Java 语言规范中,所有异常都是 Throwable 类或者其子类的实例.Throwable 有两大直接子类.

    • 第一个是 Error,涵盖程序不应捕获的异常.当程序触发 Error 时,它的执行状态已经无法恢复,需要中止线程甚至是中止虚拟机.
    • 第二子类则是 Exception,涵盖程序可能需要捕获并且处理的异常.
  • Exception 有一个特殊的子类 RuntimeException,用来表示“程序虽然无法继续执行,但是还能抢救一下”的情况.前边提到的数组索引越界便是其中的一种.

  • RuntimeException 和 Error 属于 Java 里的非检查异常(unchecked exception).其他异常则属于检查异常(checked exception).在 Java 语法中,所有的检查异常都需要程序显式地捕获,或者在方法声明中用 throws 关键字标注.通常情况下,程序中自定义的异常应为检查异常,以便最大化利用 Java 编译器的编译时检查.

  • 异常实例的构造十分昂贵.这是由于在构造异常实例时,Java 虚拟机便需要生成该异常的栈轨迹(stack trace).该操作会逐一访问当前线程的 Java 栈帧,并且记录下各种调试信息,包括栈帧所指向方法的名字,方法所在的类名、文件名,以及在代码中的第几行触发该异常.

  • 当然,在生成栈轨迹时,Java 虚拟机会忽略掉异常构造器以及填充栈帧的 Java 方法(Throwable.fillInStackTrace),直接从新建异常位置开始算起.此外,Java 虚拟机还会忽略标记为不可见的 Java 方法栈帧.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: iconst_1
1: iconst_0
2: idiv
3: istore_1
4: goto 20
7: astore_1
8: new #3 // class java/lang/RuntimeException
11: dup
12: aload_1
13: invokespecial #4 // Method java/lang/RuntimeException."<init>":(Ljava/lang/Throwable;)V
16: athrow
17: astore_2
18: aload_2
19: athrow
20: return
Exception table:
from to target type
0 4 7 Class java/lang/Exception
0 4 17 any
7 18 17 any
  • 编译过后,该方法的异常表拥有一个条目.
    • from 指定字节码索引的开始位置
    • to 指定字节码索引的结束位置
    • target 异常处理的起始位置
    • type 异常类型
    • 只要在 from 和 to 之间发生了异常,就会跳转到 target 所指定的位置。
  • 当程序触发异常时,Java 虚拟机会从上至下遍历异常表中的所有条目.当触发异常的字节码的索引值在某个异常表条目的监控范围内,Java 虚拟机会判断所抛出的异常和该条目想要捕获的异常是否匹配.如果匹配,Java 虚拟机会将控制流转移至该条目 target 指针指向的字节码.
  • 如果遍历完所有异常表条目,Java 虚拟机仍未匹配到异常处理器,那么它会弹出当前方法对应的 Java 栈帧,并且在调用者(caller)中重复上述操作.在最坏情况下,Java 虚拟机需要遍历当前线程 Java 栈上所有方法的异常表.
  • finally 代码块的编译比较复杂.当前版本 Java 编译器的做法,是复制 finally 代码块的内容,分别放在 try-catch 代码块所有正常执行路径以及异常执行路径的出口中.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ExceptionTest {

private int tryBlock;
private int catchBlock;
private int finallyBlock;
private int methodExit;

public void test() {
try {
tryBlock = 0;
} catch (Exception e) {
catchBlock = 1;
} finally {
finallyBlock = 2;
}
methodExit = 3;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public void test();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: iconst_0
2: putfield #2 // Field tryBlock:I
5: aload_0
6: iconst_2
7: putfield #3 // Field finallyBlock:I
10: goto 35
13: astore_1
14: aload_0
15: iconst_1
16: putfield #5 // Field catchBlock:I
19: aload_0
20: iconst_2
21: putfield #3 // Field finallyBlock:I
24: goto 35
27: astore_2
28: aload_0
29: iconst_2
30: putfield #3 // Field finallyBlock:I
33: aload_2
34: athrow
35: aload_0
36: iconst_3
37: putfield #6 // Field methodExit:I
40: return
Exception table:
from to target type
0 5 13 Class java/lang/Exception
0 5 27 any
13 19 27 any
  • 可以看到,编译结果包含三份 finally 代码块.其中,前两份分别位于 try 代码块和 catch 代码块的正常执行路径出口.最后一份则作为异常处理器,监控 try 代码块以及 catch 代码块.它将捕获 try 代码块触发的、未被 catch 代码块捕获的异常,以及 catch 代码块触发的异常.