前几天在公众号看到一篇文章,通过不同实例代码的方式演示了finally语句在return之前还是之后执行。然而,这个问题对于JVM的指令的逻辑顺序是什么样子的?对于最近在重温JVM的我来说,这是远远不能满足我最知识的好奇的。那么下面就让我们看一下指令代码具体的逻辑方式。
1.实例代码1
1.1 代码
如下是一段很简单的代码,通过查看指令代码,就不需要所谓的main方法了。
/**
* @author surpass
* @date 2019/11/2
*/
public class TryCatchFinally {
public int tryCatchFinallyTest(){
int a = 2;
try{
a = a + 3;
return a;
}catch (Exception e){
a += 4;
return a;
}finally {
a += 5;
return a;
}
}
}
1.2.生成指令代码文件
通过上述代码找到class文件,在当前目录下执行如下命令。
javap -v TryCatchFinally.class
我们主要截取去tryCatchFinallyTest方法生成的代码部分。
public int tryCatchFinallyTest();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=5, args_size=1
0: iconst_2
1: istore_1
2: iload_1
3: iconst_3
4: iadd
5: istore_1
6: iload_1
7: istore_2
8: iinc 1, 5
11: iload_1
12: ireturn
13: astore_2
14: iinc 1, 4
17: iload_1
18: istore_3
19: iinc 1, 5
22: iload_1
23: ireturn
24: astore 4
26: iinc 1, 5
29: iload_1
30: ireturn
Exception table:
from to target type
2 8 13 Class java/lang/Exception
2 8 24 any
13 19 24 any
24 26 24 any
LineNumberTable:
line 9: 0
line 11: 2
line 12: 6
line 17: 8
line 18: 11
line 13: 13
line 14: 14
line 15: 17
line 17: 19
line 18: 22
line 17: 24
line 18: 29
LocalVariableTable:
Start Length Slot Name Signature
14 10 2 e Ljava/lang/Exception;
0 31 0 this Lcn/surpass/jvm/review/TryCatchFinally;
2 29 1 a I
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 13
locals = [ class cn/surpass/jvm/review/TryCatchFinally, int ]
stack = [ class java/lang/Exception ]
frame_type = 74 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
通过上面生成的代码中Code的下一行有这么一段代码:stack=1, locals=5, args_size=1。他表示的意思是操作数栈为2,局部变量表示6,传入参数为1。这里有两个地方有没有感到好奇
1)在源代码中的这个方法我们明明看到的是a和e两个局部变量,为什么这里生成的反编译文件中局部变量表为6;
2)方法中我们明明没有传入参数,为什么这里的args_size为1。如果大家感兴趣,换成一个静态方法,看看有什么不同。
好了,我们接对这段代码进行详细的分析。下表就是我对方法中code部分进行了详细的注释,也许下面的分析中会对上面两个问题进行解释一下。
1.3 指令代码解释
大家可以参考如下链接https://download.csdn.net/download/oyinhezhiguang/11916883,当然也可以从网上下载JVM指令。
从上面的代码解释我们可以看出来,真正起作用的还是最后一个return。
1.4 Exception table
Exception table:
from to target type
2 8 13 Class java/lang/Exception
2 8 24 any
13 19 24 any
24 26 24 any
我们先解释一下下面异常表的意思。from 到to表示从第几行到第几行(当然行号和上面图片图中指令“:”前面的数一直),targer是要跳转的行号,type是遇见说明类型进行跳转。例如2 2 13 Class java/lang/Exception表示从指令行号为2到指令行号为8的这段代码,如果出现java/lang/Exception异常,直接跳转到指令13行,剩下的自己解释一下。然而,这里大家可以自己想如下几个问提。
1)为什么2,8出现了两次,分别为指向13行和24行。
2)13-19行中,也就是catch中的代码,如果出现异常,也会跳转到24行。
3)24-26行出现异常,也会跳回24行,这样又是一个什么逻辑。如果在finally出现异常,会不会出现死循环的情况。
2. 实例代码2
2.1 代码
/**
* @author surpass
* @date 2019/11/2
*/
public class TryCatchFinally {
public int tryCatchFinallyTest(){
int a = 2;
try{
a = a + 3;
return a;
}catch (Exception e){
a += 4;
return a;
}finally {
a += 5;
}
}
}
和实例1代码不一致的地方就是去掉了finally中的return,你们他生成的反编译代码为.
public int tryCatchFinallyTest();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=5, args_size=1
0: iconst_2
1: istore_1
2: iload_1
3: iconst_3
4: iadd
5: istore_1
6: iload_1
7: istore_2
8: iinc 1, 5
11: iload_2
12: ireturn
13: astore_2
14: iinc 1, 4
17: iload_1
18: istore_3
19: iinc 1, 5
22: iload_3
23: ireturn
24: astore 4
26: iinc 1, 5
29: aload 4
31: athrow
Exception table:
from to target type
2 8 13 Class java/lang/Exception
2 8 24 any
13 19 24 any
24 26 24 any
LineNumberTable:
line 9: 0
line 11: 2
line 12: 6
line 17: 8
line 12: 11
line 13: 13
line 14: 14
line 15: 17
line 17: 19
line 15: 22
line 17: 24
line 18: 29
LocalVariableTable:
Start Length Slot Name Signature
14 10 2 e Ljava/lang/Exception;
0 32 0 this Lcn/surpass/jvm/review/TryCatchFinally;
2 30 1 a I
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 13
locals = [ class cn/surpass/jvm/review/TryCatchFinally, int ]
stack = [ class java/lang/Exception ]
frame_type = 74 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
}
2.2 差异比较
上面我们比价详细说了字节码的含义,现在我们分析一下这两个实例的代码差异所在如下图。
还记得上面提到的为什么这段代码明面上只有两个局部变量,反编译之后就出现了5个局部变量。每个代码块的return之前都会将返回的数据或者引用都要复制出来重新放置到一个局部变量表中。我们仔细发现差异就是在ireturn指令之前压如栈顶的局部变量表不一致,还有29-30(31)中返回的内容不一致。当指令执行7#时,try中的return指向的是iload_2(只是还没有执行而已),此时我认为也就是将iload_2放入到方法出口中。当执行到12#时候,对于finally有return的语句,finally中的return会指向iload_1,此时会覆盖方法出口的信息,而对于finally中没有return的,方法出口还是保持原来的iload_2。所以会呈现出不同的结果。
3. 总结
3.1 他们的执行逻辑顺序分为:try->finally、try->catch->finally
3.2 通过指令顺序我们发现try中的return和catch中的return失效了,也就是上面分析提到的方法出口覆盖的情况。
3.3 不管是怎么是哪种情况,遇到return,会把当前的变量的副本指向方法出口,代码继续执行(一般为finally中的代码),直到执行完毕后执行方法出口。
3.4 我认为return是分两步执行的,第一步是将引用或者变量副本指向方法出口,第二步是执行方法出口,然而,finally代码块在return的这两步中间执行。