写在前面

本文隶属于专栏《100个问题搞定Java虚拟机》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!

本专栏目录结构和文献引用请见100个问题搞定Java虚拟机

解答

Java 字节码中,每个方法对应一个异常表。
当程序触发异常时,Java 虚拟机将查找异常表,并依此决定需要将控制流转移至哪个异常处理器之中。
Java 代码中的 catch 代码块和 finally 代码块都会生成异常表条目。

补充

异常表

在编译生成的字节码中,每个方法都附带一个异常表。

异常表中的每一个条目代表一个异常处理器,并且由 from 指针、to 指针、target 指针以及所捕获的异常类型构成。

这些指针的值是字节码索引(bytecode index,bci),用以定位字节码。

其中,from 指针和 to 指针标示了该异常处理器所监控的范围,例如 try 代码块所覆盖的范围。

target 指针则指向异常处理器的起始位置,例如 catch 代码块的起始位置。

当程序触发异常时,Java 虚拟机会从上至下遍历异常表中的所有条目。

当触发异常的字节码的索引值在某个异常表条目的监控范围内,Java 虚拟机会判断所抛出的异常和该条目想要捕获的异常是否匹配。

如果匹配,Java 虚拟机会将控制流转移至该条目 target 指针指向的字节码。

如果遍历完所有异常表条目,Java 虚拟机仍未匹配到异常处理器,那么它会弹出当前方法对应的 Java 栈帧,并且在调用者(caller)中重复上述操作。

在最坏情况下,Java 虚拟机需要遍历当前线程 Java 栈上所有方法的异常表。

finally 的实现原理

finally 代码块的编译比较复杂。

当前版本 Java 编译器的做法,是复制 finally 代码块的内容,分别放在 try-catch 代码块所有正常执行路径以及异常执行路径的出口中。

针对异常执行路径,Java 编译器会生成一个或多个异常表条目,监控整个 try-catch 代码块,并且捕获所有种类的异常(在 javap 中以 any 指代)。

这些异常表条目的 target 指针将指向另一份复制的 finally 代码块。

并且,在这个 finally 代码块的最后,Java 编译器会重新抛出所捕获的异常。

finally

Suppressed 与 try-with-resources

Java 7 的 Suppressed 异常以及语法糖Java 7 引入了 Suppressed 异常允许开发人员将一个异常附于另一个异常之上。

因此,抛出的异常可以附带多个异常的信息

然而,Java 层面的 finally 代码块缺少指向所捕获异常的引用,所以这个新特性使用起来非常繁琐。

为此,Java 7 专门构造了一个名为 try-with-resources 的语法糖,在字节码层面自动使用 Suppressed 异常。

当然,该语法糖的主要目的并不是使用 Suppressed 异常,而是精简资源打开关闭的用法。

在 Java 7 之前,对于打开的资源,我们需要定义一个 finally 代码块,来确保该资源在正常或者异常执行状况下都能关闭。资源的关闭操作本身容易触发异常。

因此,如果同时打开多个资源,那么每一个资源都要对应一个独立的 try-finally 代码块,以保证每个资源都能够关闭。这样一来,代码将会变得十分繁琐。

上一篇 下一篇