1. 缘由
我们经常会被问到try中如果return了,finally中的代码是否还会执行?如果在finally中修改返回值大小,是否会影响返回值等问题,这些问题我们可以根据try catch finally的执行逻辑死记硬背,但是时间一长记忆就会发生形变,让问题变得既熟悉又陌生,不能准确给出答案,因此本文尝试通过字节码的方式,来解释try catch finally的本质执行逻辑,透过现象看本质,记忆会更为深刻
2. 站在字节码角度看待问题
下面我会给出一个方法,以及该方法编译后得到的字节码
private static int get() {
int i = 1;
try {
i += 1;
return i;
} catch (IllegalArgumentException e) {
System.out.println("IllegalArgumentException");
} catch (Exception e) {
System.out.println("Exception");
} finally {
i += 2;
}
System.out.println(i);
return i;
}
private static int get();
descriptor: ()I
flags: (0x000a) ACC_PRIVATE, ACC_STATIC
Code:
stack=2, locals=3, args_size=0
0: iconst_1
1: istore_0
2: iinc 0, 1
5: iload_0
6: istore_1
7: iinc 0, 2
10: iload_1
11: ireturn
12: astore_1
13: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
16: ldc #27 // String IllegalArgumentException
18: invokevirtual #29 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
21: iinc 0, 2
24: goto 48
27: astore_1
28: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
31: ldc #34 // String Exception
33: invokevirtual #29 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
36: iinc 0, 2
39: goto 48
42: astore_2
43: iinc 0, 2
46: aload_2
47: athrow
48: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
51: iload_0
52: invokevirtual #19 // Method java/io/PrintStream.println:(I)V
55: iload_0
56: ireturn
Exception table:
from to target type
2 7 12 Class java/lang/IllegalArgumentException
2 7 27 Class java/lang/Exception
2 7 42 any
12 21 42 any
27 36 42 any
可以看到,上面字节码总共分为了下面几块:
2.1 正常执行路径(无异常)
2.1.1. 初始化局部变量 i
0: iconst_1 // 将 int 1 压入操作数栈
1: istore_0 // 将栈顶的 1 存储到局部变量槽位 0(假设为变量 `a`)
2.1.2. try 块逻辑
2: iinc 0, 1 // 对槽位 0 的值加 1(i = 1 + 1 → 2)
5: iload_0 // 将槽位 0 的值(i=2)加载到栈顶
6: istore_1 // 将栈顶的 2 存储到槽位 1(假设为临时变量 `temp`)
7: iinc 0, 2 // 对槽位 0 的值加 2(i = 2 + 2 → 4)finally中的逻辑
10: iload_1 // 加载槽位 1 的值(temp=2)到栈顶
11: ireturn // 返回栈顶的 2(方法提前返回)
解释下上面几个字节码的作用:
iload指令:将局部变量表中指定槽位的int类型变量加载到操作数栈顶
istore指令:将操作数栈顶的int值弹出,存储到局部变量表的指定槽位
iinc指令:直接对局部变量表中指定槽位的int值进行自增/自减操作,无需操作数栈参与
好了,现在我们来看上面字节码让人摸不着头脑的地方,字节码第6行及后续部分,2和5我们都还明白,就是对应的i+=1;,第6行将栈顶2存储到局部变量表槽位1,后续所有操作都跟这个槽位没有关系,第7行将槽位0的值+2,第10行加载槽位0的值到栈顶,第11行返回栈顶元素
这就是我们常说的,try中如果有return,会将结果暂存,等执行完finally代码后,再将暂存的结果返回,因此上面第6行及后续部分的含义我们就清楚了,现将try中计算的结果2暂存,然后执行finally逻辑,finally中如果有对返回值的改动,并不影响暂存值,因为暂存值此时已经快照了,原变量的值确实会变化,但是返回的不是原变量i的值,而是try中执行完return时快照的暂存的值,即上述方法在正常执行情况下,返回结果为2,不是4.
2.2 异常处理路径
2.2.1 捕获 IllegalArgumentException
12: astore_1 // 将异常对象存储到槽位 1(覆盖之前的 `temp`)
13: getstatic #7 // 获取 System.out 对象
16: ldc #27 // 加载字符串 "IllegalArgumentException"
18: invokevirtual #29 // 调用 println 打印字符串
21: iinc 0, 2 // 对槽位 0 的值加 2(i = 2 + 2 → 4)finally中的逻辑
24: goto 48 // 跳转到指令 48
2.2.2 捕获 Exception
27: astore_1 // 将异常对象存储到槽位 1
28: getstatic #7 // 获取 System.out 对象
31: ldc #34 // 加载字符串 "Exception"
33: invokevirtual #29 // 调用 println 打印字符串
36: iinc 0, 2 // 对槽位 0 的值加 2(i = 2 + 2 → 4)finally中的逻辑
39: goto 48 // 跳转到指令 48
2.2.3 最终处理(finally 逻辑,未捕获异常的抛出路径)
42: astore_2 // 将未捕获的异常对象存储到槽位 2
43: iinc 0, 2 // 对槽位 0 的值加 2(i = 2 + 2 → 4)finally中的逻辑
46: aload_2 // 加载槽位 2 的异常对象到栈顶
47: athrow // 重新抛出异常
2.3 最终输出与返回
48: getstatic #7 // 获取 System.out 对象
51: iload_0 // 加载槽位 0 的值(i)到栈顶
52: invokevirtual #19 // 调用 println 打印 i 的值
55: iload_0 // 再次加载 i 的值到栈顶
56: ireturn // 返回 i 的值
2.4 异常表分析
Exception table:
from to target type
2 7 12 Class java/lang/IllegalArgumentException
2 7 27 Class java/lang/Exception
2 7 42 any
12 21 42 any
27 36 42 any
-
前三行:
try块(指令 2-7)抛出异常时的处理逻辑:- 若抛出
IllegalArgumentException,跳转到 12。 - 若抛出
Exception,跳转到 27。 - 若抛出其他异常,跳转到 42(
finally块)。
- 若抛出
-
后两行:在
catch块(12-21 和 27-36)中再次抛出异常时,跳转到 42(finally块)。
3. 核心
try catch finally语法糖实现的核心主要是异常表和代码复制,这两个特性在上面例子中均有体现,异常表很好理解,参见2.4即可,代码复制也很好理解,finally中的代码编译后的字节码,在下述所有位置都会复制一遍来插入:
try块正常结束后的代码路径- 每个
catch块处理完异常后的代码路径 - 未捕获异常的抛出路径(通过
athrow指令)
在上面的字节码中我们也能看到try、catch以及未捕获异常的抛出路径中均有相关字节码,至此,我们从字节码层面,通过代码复制、异常表、返回值暂存等概念,就能非常清晰的理解try catch finally的执行逻辑了