Java虚拟机在运行Java方法时,并不会在每条指令中都嵌入异常判断逻辑,而是把方法中所有的异常处理范围集中记录在一张异常表中。当方法执行期间抛出未捕获的异常,虚拟机就会用当前指令地址去异常表里查找该由哪段catch逻辑接管。

异常表在class文件中的位置
异常表是Code属性的一部分。每个有异常处理逻辑的方法,在编译后的字节码里都会包含一个Code属性,其中除了字节码指令和局部变量表,还有一张exception_table。这张表由若干条目组成,每条对应一个try catch或者try finally的捕获范围。
异常表的格式
异常表中每一条记录包含四个关键字段,可以用下面的表格说明:
| 字段 | 含义 |
|---|---|
| start_pc | 异常捕获起始指令位置,包含该位置 |
| end_pc | 异常捕获结束指令位置,不包含该位置 |
| handler_pc | 异常处理器的起始指令位置,即catch块入口 |
| catch_type | 要捕获的异常类索引,0表示捕获所有异常(finally) |
异常表的查找机制
当方法执行中抛出异常时,虚拟机首先记录当前程序计数器的值,也就是出错的那条指令位置。然后按顺序扫描异常表,找到第一个满足下面条件的条目:
- 当前pc大于等于start_pc
- 当前pc小于end_pc
- 抛出的异常类型是catch_type指向的类或其子类,或者catch_type为0
如果找到匹配项,程序计数器被设为handler_pc,继续执行catch块。如果没找到,当前方法立刻结束,异常向外层调用栈传播,重复同样的查找过程。
字节码层面的例子
下面是一段简单的Java代码以及其对应的近似字节码结构,帮助理解异常表如何工作:
// Java源码
public void demo() {
try {
int a = 1 / 0;
} catch (ArithmeticException e) {
System.out.println("error");
}
}
// 编译后异常表逻辑(示意)
// start_pc=0, end_pc=4, handler_pc=7, catch_type=ArithmeticException
// 0: iconst_1
// 1: iconst_0
// 2: idiv
// 3: istore_1
// 4: goto 12
// 7: astore_2
// 8: getstatic
// 11: invokevirtual
// 12: return
多个catch的查找顺序
异常表中的条目顺序和源码中catch的书写顺序一致。虚拟机从上往下匹配,因此写catch时要把子类异常放在前面,否则父类异常会先匹配,导致后面的catch永远无法执行。编译器也会做基本检查,但理解机制能避免逻辑错误。
finally的实现方式
finally并不是独立语法,编译器会把它变成catch_type为0的异常表项,并且在正常路径和异常路径都插入finally代码。这样无论是否抛异常,finally逻辑都会运行。如果finally中也抛异常,原始异常会被覆盖。
小结
Java虚拟机通过异常表把异常处理从指令流中剥离出来,用范围加类型的方式做运行时查找。掌握异常表格式与查找机制,能让我们在读字节码、分析性能以及处理嵌套异常时更加从容。