try catch finally如何执行异常时跳转?finally 语句为何一定会执行?
其背后的原因值得深究,我们从JVM的角度看看try catch finally
这个语法糖背后的实现原理。
JVM 如何执行 try - catch
创建一个TryCatchFinallyDemo.java 类,在foo()方法中声明了try-catch block;声明了 handleException 这个空方法。
使用 javac 指令将其编译为class 文件,并使用javap -c -v -s 查看结果。相对于没有try-catch block 的代码,下列代码中多出了一个Exception Table。
在编译后字节码中,每个方法都附带一个异常表(Exception table),异常表里的每一行表示一个异常处理器,由 from 指针、to 指针、target 指针、所捕获的异常类型 type 组成。
这些指针的值是字节码索引,用于定位字节码 其含义是在[from, to)
字节码范围内,抛出了异常类型为type
的异常,就会跳转到target
表示的字节码处。
比如,上面的例子异常表表示:在0到3中间(不包含 3)如果抛出了Exception异常,就跳转到6执行。
多个catch 语句
下面举一个有多个catch 语句的例子,虽然下面三个异常不会发生。
使用javac -s 可以简单看到对应的ctach 块字节码。Exception Table 中变为三种类型的异常,如果[0,3)的代码段(不包括3)发生异常,则可以跳转到,6,15,24行代码寻找可捕获的异常类型。
当程序出现异常时,Java 虚拟机会从上至下遍历异常表中所有的条目。当触发异常的字节码索引值在某个异常条目的[from, to)
范围内,则会判断抛出的异常与该条目想捕获的异常是否匹配。
- 如果匹配,Java 虚拟机会将控制流跳转到 target 指向的字节码;如果不匹配则继续遍历异常表
- 如果遍历完所有的异常表,还未匹配到异常处理器,那么该异常将蔓延到调用方(caller)中重复上述的操作。最坏的情况下虚拟机需要遍历该线程 Java 栈上所有方法的异常表。 如果在方法栈中所有的调用方中,都未找到可匹配的异常表,JVM会清空当前方法栈。
finally 分析
finally 始终执行的秘密
那么,JVM如何保证finally 关键字始终执行呢?我们为上述例子增加一个finally block。
重新编译后使用javap -c 查看。
可以看到,字节码中包含了三份 finally 语句块,都在程序正常 return 和异常 throw 之前。其中两处在 try 和 catch 调用 return 之前,一处是在异常 throw 之前。
Java 采用方式是复制 finally 代码块的内容,分别放在 try catch 代码块所有正常 return 和 异常 throw 之前。所以finally 代码块始终会执行。
下面有两个并不常用的场景。
finally 修改返回值场景
下列代码运行结果是1, 还是3呢?这个场景能迷惑到不少人。我们执行一边,结果是1。
编译查看字节码:
通过字节码,我们发现,在try语句的return块中,return 返回的变量并不是直接返回 i 值,而是在执行finally块之前把i值存储在临时区域,当执行return时直接返回的临时区域中的值,即使在finally语句中把变量 i 的值修改了,也不会影响返回的值。
finally 中有return 的场景
当finally 中有return 语句时,return 语句会重写 try-block, catch-block的返回值。
略修改上个章节的例子:在finally 语句中增加一行返回值操作。运行结果却变成了3, 返回了finally block 中的值。
编译后查看字节码,并与上个章节的例子做对比。左侧是上个章节编译后的字节码,右侧是上面例子编译的字节码。
每个try block, catch block 后侧,return指令之前,都会拷贝finally block的代码块。可以看到,虽然try-catch block中的i值被暂存了,但是由于finally 有return 语句,返回的依然是finally 修改后的i值。
总结
- 第一,JVM 采用异常表的方式来处理 try-catch 的跳转逻辑;
- 第二,finally 的实现采用拷贝 finally 语句块的方式来实现 finally 一定会执行的语义逻辑;
- 第三,讲解了在 finally 中有 return 语句或者 抛异常的情况。