字节码角度看面试题 —— try catch finally 为啥 finally 语句一定会执行
一、try catch 字节码分析
1.1 一个 catch
public class Test {
public void foo() {
try {
tryItOut();
} catch (MyException e) {
handleException(e);
}
}
private void tryItOut() throws MyException {
}
class MyException extends Exception {
}
private void handleException(MyException e) {
}
}
javap 查看字节码
javap -c -v Test
复制 foo() 函数部分,如下
public void foo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=1
0: aload_0
1: invokespecial #15 // Method tryItOut:()V
4: goto 13
7: astore_1
8: aload_0
9: aload_1
10: invokespecial #18 // Method handleException:(LTest$MyException;)V
13: return
Exception table:
from to target type
0 4 7 Class Test$MyException
14 ~ 16 行:在生成的字节码中,每个方法都会附带一个异常表(Exception table),其中每一行表示一个异常处理器,由 from 指针、to 指针、target 指针、所捕获的异常类型 type 组成
这些指针的值是字节码索引,用于定位字节码,其含义是在 [from, to) 字节码范围内,抛出异常类型为 type 的异常,就会跳转到 target 表示的字节码处
上面的栗子,异常表表示:在 0 到 4 中间(不含 4)如果抛出了 MyException 的异常,就跳转到 7 执行
- 9 行:astore_1 指令处于异常表范围内,因此表示将异常对象 Test$MyException 的引用放到局部变量表下标为 1 的位置
- 10 行:aload_0 指令表示将 this(非静态方法,局部变量表第 0 个位置默认就是 this)引用放到操作数栈栈顶
- 11 行:aload_1 指令表示将对象 Test$MyException 的引用放到操作数栈栈顶
- 12 行:invokespecial 指令表示弹出栈顶的两个元素,去调用 handleException() 方法
- 13 行:return 指令表示函数执行完了,返回
1.2 多个 catch
修改上面的栗子,多加几个 catch 块
public class Test {
public void foo() {
try {
tryItOut();
} catch (MyException e) {
handleException(e);
} catch (MyException1 e) {
handleException1(e);
}
}
private void tryItOut() throws MyException, MyException1 {
}
class MyException extends Exception {
}
private void handleException(MyException e) {
}
class MyException1 extends Exception {
}
private void handleException1(MyException1 e) {
}
}
javap 查看字节码
javap -c -v Test
复制 foo() 函数部分,如下
public void foo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=1
0: aload_0
1: invokespecial #15 // Method tryItOut:()V
4: goto 22
7: astore_1
8: aload_0
9: aload_1
10: invokespecial #18 // Method handleException:(LTest$MyException;)V
13: goto 22
16: astore_1
17: aload_0
18: aload_1
19: invokespecial #22 // Method handleException1:(LTest$MyException1;)V
22: return
Exception table:
from to target type
0 4 7 Class Test$MyException
0 4 16 Class Test$MyException1
很明显可以看出,异常表多了一行记录
Java 虚拟机会从上到下遍历异常表中所有的条目。当触发异常表的字节码索引值在某个异常条目范围内,则会判断抛出的异常与该条目想捕获的异常是否匹配,当前栗子即为在 0 到 4 中间(不含 4)可能会抛出 MyException 或 MyException1 异常,具体抛哪个,虚拟机会去匹配实际抛出的异常与该条目想捕获的异常是否匹配
- 如果匹配,Java 虚拟机会将控制流跳转到 target 指向的字节码;若不匹配,则继续遍历异常表
- 如果遍历完异常表的所有异常条目,还未匹配到异常处理器,那么该异常将蔓延到调用方中重复上述操作。最坏的情况下虚拟机需要遍历该线程 Java 栈上所有方法的异常表
二、finally 字节码分析
public class Test {
public void foo() {
try {
tryItOut();
} catch (MyException e) {
handleException(e);
} finally {
handleFinally();
}
}
private void tryItOut() throws MyException {
}
class MyException extends Exception {
}
private void handleException(MyException e) {
}
private void handleFinally() {
}
}
javap 查看字节码
javap -c -v Test
复制 foo() 函数部分,如下
public void foo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: invokespecial #15 // Method tryItOut:()V
4: goto 27
7: astore_1
8: aload_0
9: aload_1
10: invokespecial #18 // Method handleException:(LTest$MyException;)V
13: aload_0
14: invokespecial #22 // Method handleFinally:()V
17: goto 31
20: astore_2
21: aload_0
22: invokespecial #22 // Method handleFinally:()V
25: aload_2
26: athrow
27: aload_0
28: invokespecial #22 // Method handleFinally:()V
31: return
Exception table:
from to target type
0 4 7 Class Test$MyException
0 13 20 any
24 ~ 27 行:表示在 0 到 4 中间(不含 4)如果抛出了 MyException 的异常,就跳转到 7 执行;在 0 到 13 中间(不含 13)如果抛出了 any 类型的异常(也就是未捕获的异常),就跳转到 20 执行
- 6 行:aload_0 指令表示将 this 引用放到操作数栈栈顶
- 7 行:invokespecial 指令表示调用 tryItOut() 方法
- 8 行:goto 27 指令表示程序流程跳转到 27(21 行),aload_0 指令表示将 this 引用放到操作数栈栈顶
- 9 行:首先要明白 try 代码块中的代码按理说已经执行完了,如果还能执行到走到 9 行,只能说明遇到异常了,另一方面也能知道,因为 9 行对应的 7 在异常表范围内,所以发生了异常,那么astore_1 指令表示将异常对象 MyException 的引用放到局部变量表下标为 1 的位置
- 10 行:aload_0 指令表示将 this 引用放到操作数栈栈顶
- 11 行:aload_1 指令表示将异常对象 Test$MyException 的引用放到操作数栈栈顶
- 12 行:invokespecial 指令表示弹出栈顶两个元素,去调用 handleException() 方法
- 13 行:aload_0 指令表示将 this 引用放到操作数栈栈顶
- 14 行:invokespecial 指令表示调用 handleFinally() 方法
- 15 行:goto 31 指令表示程序流程跳转到 31(23 行),return 指令表示函数执行完了,返回
- 16 行:16 行对应 20,在异常表的范围内,因此 astore_2 指令表示将异常对象 Exception 的引用放到局部变量表下标为 2 的位置
- 17 行:aload_0 指令表示将 this 引用放到操作数栈栈顶
- 18 行:invokespecial 指令表示调用 handleFinally() 方法
- 19 行:aload_2 指令表示将异常对象 Exception 的引用放到操作数栈栈顶
- 20 行:athrow 指令表示抛 catch 代码块执行过程中出现的异常
- 21 行:aload_0 指令表示将 this 引用放到操作数栈栈顶
- 22 行:invokespecial 指令表示调用 handleFinally() 方法
- 23 行:return 指令表示函数执行完了,返回
上面的字节码其实对应代码的几种可能的执行流程
情况 ①:try 代码块正常执行,没有异常,对应上面的字节码第 6、7、8、21、22、23 行
// 当前情况下,22 行对应 finally 代码块的执行
情况 ②:try 代码块执行过程中遇到异常,且该异常在异常表范围内有匹配的异常(也就是说有对应的 catch 块捕获了出现的异常),对应上面的字节码第 6、7、9、10、11、12、13、14、15、23 行
// 当前情况下,14 行对应 finally 代码块的执行
情况 ③:try 代码块执行过程中遇到异常,且该异常在异常表范围内有匹配的异常,但是 catch 块中的代码执行过程中出现了异常,对应上面的字节码第 6、7、9、10、11、12、13、16、17、18、19、20 行
// 当前情况下,18 行对应 finally 代码块的执行,注意 throw 指令执行是在 20 行,也就是说如果在 catch 代码块中即使出现异常,也会先执行 finally 代码块,然后再抛 catch 块中出现的异常
综上所述,不管什么情况下,finally 代码块都会得到执行(●°u°●) 」
三、小结
- JVM 采用异常表的方式来处理 try catch 的跳转逻辑
- finally 的实现采用拷贝 finally 语句块的方式来实现 finally 一定会执行的语义逻辑
四、留道题
public static int foo() {
int x = 0;
try {
return x;
} finally {
++x;
}
}
public static void main(String[] args) {
int res = foo();
System.out.println(res);
}
返回 0