原文来自我的个人博客 twodam.net
预备知识
这里只提到部分涉及到的知识点,详情查看
JVM栈
在每个JVM线程被创建的时候,与之对应的私有JVM栈也会被创建。
JVM栈存储栈帧。类似于C语言中的栈,JVM栈中包含局部变量和部分的结果,同时在方法调用和返回中起到一定的作用。
栈帧
每一时刻在一个线程中,只有一个正在执行的方法的栈帧是激活的。这个栈帧我们叫做“当前栈帧”,对应的方法叫做“当前方法”。对局部变量和操作数栈的操作一般都与当前栈帧有关。
当一个方法被调用,控制转移到新的方法时,一个新的栈帧会被创建并成为当前栈帧。
当一个方法返回时,当前栈帧将其方法调用的结果返回到之前的栈帧。当前栈帧随后被废弃,之前的栈帧成为当前栈帧。
局部变量
通过索引对局部变量进行寻址。第一个局部变量的索引是0。
操作数栈
每个栈帧都有一个LIFO的栈,叫做操作数栈。
栈帧刚创建时,其中的操作数栈是空的。JVM提供了将常量、局部变量或是字段的值推入操作数栈的指令。其他JVM指令则将操作数从操作数栈上取下,对它们进行操作,然后将结果推回操作数栈。操作数栈也被用于准备传递给方法的参数或是接受方法的结果。
异常与finally
生成50.0或更低版本class文件的Java编译器可能会通过异常处理机制和两种特殊的指令: jsr("jump to subrutine") 和 ret("return from subroutine")来实现try-finally结构。
当子程序执行jsr指令时,它会把自己的返回地址,也就是jsr之后将被执行的命令的地址作为returnAddress类型的数据值推入操作数栈。子程序会把返回值存储在一个局部变量中。在子程序的最后,ret指令会从局部变量中取回返回地址,然后将控制转移到指定返回地址的那条指令上。
finally语句在JVM代码中会编译成它所在方法的一个子程序。
控制可以通过几种不同的方式传递给finally子句。如果try语句正常完成,finally子程序会在执行下一条表达式之前通过jsr命令被执行。try语句中的break或continue语句会在将控制转移到try语句外之前先执行jsr指令跳转到finally语句并执行。如果try语句中执行了return,编译后的代码会这样做:
- 如果有返回值则将其存储在一个局部变量中。
- 执行jsr指令跳转到finally语句。
- 从finally语句中向上返回,返回值从之前保存的局部变量中取。
编译器会准备一个特殊的异常处理器来捕获try语句中抛出的任何异常。如果执行try语句时有异常抛出,这个异常处理器或这样做:
- 将异常保存在一个局部变量中。
- 执行jsr指令跳转到finally语句。
- 从finally语句中向上返回,重新抛出异常。
一个实例
static int test() {
int x = 1;
try {
return x;
} finally {
++x;
}
}
这个方法的返回值是多少?
前面我们看了JVM的规范,这里我们通过查看字节码的方式来研究一下:
static int test();
Code:
0: iconst_1
1: istore_0
2: iload_0
3: istore_1
4: iinc 0, 1
7: iload_1
8: ireturn
9: astore_2
10: iinc 0, 1
13: aload_2
14: athrow
Exception table:
from to target type
2 4 9 any
下面我们画图来观察一下这个过程中操作数栈和局部变量表的变化:
iconst_1
将常量1入栈
索引 | 局部变量 | 操作数栈 |
---|---|---|
0 | 1 | |
1 |
istore_0
出栈并将值存入索引为0的局部变量中
索引 | 局部变量 | 操作数栈 |
---|---|---|
0 | 1 | |
1 |
iload_0
将索引为0的局部变量的值入栈
索引 | 局部变量 | 操作数栈 |
---|---|---|
0 | 1 | 1 |
1 |
istore_1
出栈并将值存入索引为1的局部变量中
索引 | 局部变量 | 操作数栈 |
---|---|---|
0 | 1 | |
1 | 1 |
iinc 0 1
将索引为0的局部变量的值加上一个常量1
索引 | 局部变量 | 操作数栈 |
---|---|---|
0 | 2 | |
1 | 1 |
iload_1
将索引为1的局部变量的值入栈
索引 | 局部变量 | 操作数栈 |
---|---|---|
0 | 2 | 1 |
1 |
由此可以看出,本例中test方法的返回值是1。
想一想,如果finally中也有return语句,那本例中test方法的返回值是多少?