你真的知道finally吗?(二)
2018-01-23 本文已影响80人
Misout
微信公众号:Misout的博客
如有问题或建议,请留言
try catch finally的执行顺序
在上一篇文章【你真的知道finally吗?(一)】中,我们可以得出如下的结论:
finally 语句块是在 try 或者 catch 中的 return 语句或控制转移语句之前执行的
由此,可以轻松的理解之前finally中的各种现象。但深入到JVM底层我们就不得而知了。所以本文我们来一起探究JVM底层对try,catch中包含return语句是怎样的过程。本文需要有一定的JVM内存结构的知识,以及VM虚拟机栈的入栈,出栈等知识。
从字节码的角度看执行顺序
回顾下昨天的测试Demo,最终finallyCase()方法返回值是3,并非4。
package com.misout.grammar;
public class FinallyTest {
public int finallyCase() {
int i = 1;
try {
i = 3;
return i;
} finally {
i = 4;
}
}
public static void main(String[] args) {
FinallyTest test = new FinallyTest();
test.finallyCase();
}
}
finallyCase()方法返回值:3
在CMD中进入FinallyTest.class所在目录,执行javap -c FinallyTest,得到如下的字节码:
public class com.misout.grammar.FinallyTest {
public com.misout.grammar.FinallyTest();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
public int finallyCase();
Code:
0: iconst_1
1: istore_1
2: iconst_3
3: istore_1
4: iload_1
5: istore_3
6: iconst_4
7: istore_1
8: iload_3
9: ireturn
10: astore_2
11: iconst_4
12: istore_1
13: aload_2
14: athrow
Exception table:
from to target type
2 6 10 any
public static void main(java.lang.String[]);
Code:
0: new #1 // class com/misout/grammar/FinallyTest
3: dup
4: invokespecial #23 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #24 // Method finallyCase:()I
12: pop
13: return
}
上面得到的字节码,是由一系列的指令符号组成。主要分为三部分:
- public com.misout.grammar.FinallyTest():构造方法的指令。不是本文的重点。
- public int finallyCase():finallyCase()方法执行的指令及执行顺序。
- public static void main(java.lang.String[]):main方法的指令。
Java编译器输出的指令流,基本上是一种基于栈的指令集架构,也就是说指令的执行依赖于虚拟机栈(Java的一种内存空间)上的操作数栈来支持。虚拟机栈是线程私有的内存空间,每个方法在执行时会在虚拟机栈上创建一个栈帧,每个方法的执行相当于在虚拟机栈上的一个入栈和出栈的过程。每个栈帧包括:局部变量表、操作数栈、返回地址等空间,用于方法执行时存放局部变量,以及利用操作数栈做计算。本文涉及到的有局部变量表和操作数栈,以及返回地址。在概念模型上,典型的栈帧模型图如下:
栈帧概念结构图为了方便读者更好的理解本文,先看下文中涉及的字节码。在后面结合操作数栈来解释原理。下面列出了方法finallyCase()涉及到的字节码指令的含义:
- 0: iconst_1:前面的0表示指令位于方法中指令的起始地址偏移。指令意思是将局部变量表中整型常量1加载到操作数栈顶,后缀1表示常量值1。
- 1: istore_1:将操作数栈顶的整型值出栈,并保存到局部变量表的第1个slot区,这里的后缀1和iconst_1中的1不同,这里的1代表局部变量表的slot位置。
- 4: iload_1:将局部变量表第1个slot区的整型值压入操作数栈顶。
- ireturn:方法的返回指令,将结束方法的执行,并将操作数栈顶的值返回给方法,作为方法的返回值给调用方。
基于栈的解释器指令执行过程
执行偏移地址为0的指令情况 执行偏移地址为1的指令情况 执行偏移地址为2的指令情况 执行偏移地址为3的指令情况 执行偏移地址为4的指令情况 执行偏移地址为5的指令情况注意:在上图中,执行偏移地址为5的指令后即将执行finally块中指令。为什么此处将栈顶的3存入局部变量表的第3个slot而不是第2个slot,实际上是对返回结果的暂存。 执行偏移地址为6的指令情况 执行偏移地址为7的指令情况 执行偏移地址为8的指令情况 执行偏移地址为9的指令情况:返回指令
分析到这里,已经得到我们想要的结果了。至于10~14的指令就不再继续分析了,有兴趣的同学可以继续查阅资料。
结论
- Java是基于栈的指令集架构语言,所有的指令的执行都是基于入栈和出栈操作。和其他语言基于寄存器的操作方式有很大区别。
- 通过上面的指令执行图,JVM在执行return语句前,会将结果值放入局部变量表的最后一个slot进行暂存,然后转去执行finally语句块。执行完成后,重新将slot暂存的值压入操作数栈然后返回。
参考
- 深入理解Java虚拟机-JVM高级特性与最佳实践(第二版) 周志明 著