操作数栈(Operand Stack)是栈帧中一个先入后出的栈,同局部变量表一样,栈的最大深度在编译期间就已确定,并在运行期间也不会改变。JVM虚拟机的解释引擎是基于栈的执行引擎,其中的栈就是操作数栈。操作数栈其实可以理解为方法执行时变量的临时存储区,存储类型与局部变量表一致,其中byte、short、char都会转为int类型,32位类型所占的栈容量为1,64位类型则占用栈容量为2(如long和double)。

一、执行原理

public class OperandStackDemo {
    public int add() {
        int x = 2;
        int y = 4;
        int z = x + y;
        return z;
    }
}
Code:
  stack=2, locals=4, args_size=1
     0: iconst_2
     1: istore_1
     2: iconst_4
     3: istore_2
     4: iload_1
     5: iload_2
     6: iadd
     7: istore_3
     8: iload_3
     9: ireturn

以上面的简单代码为例,通过javap得到字节码文件中的Code属性部分,由Code的内容可以可知操作数栈最大深度为2,局部变量最大长度为4,Code中的字节码指令是由执行引擎解释执行,操作数栈中的元素的类型必须与字节码指令的序列相匹配,像上面出现的指令都是针对int类型操作,就不能去操作long类型的元素,JVM在编译时和类校验的数据流分析时都验证这一点,先了解Code中出现的几条JVM字节码指令:

  • iconst:将一个整型常量值(-1~5)推入操作数栈栈顶,不同范围的整型值对应指令不同,如bipush(-128~127)、sipush(-32768~32767)等。
  • istore:取出一个栈顶的整型常量值存入局部变量表。
  • iload:将局部变量表的元素放入操作数栈栈顶,可以看成和istore相反的操作。
  • iadd:取出栈顶两个整型值进行相加,。
  • ireturn:结束方法执行,并将栈顶整型常量值返回给方法调用者

 

当一个Java方法开始执行时,首先线程会创建该方法的栈帧,栈帧创建空的操作数栈和局部变量表,执行过程中,会有各种指令往操作数栈中写入和读取内容,也就是出栈和入栈操作。如上图,是方法执行的过程中局部变量表、操作数栈及程序计数器的变化情况,由于是成员方法,所以执行开始时局部变量的0的索引位置会存储this变量,程序计数器中会存储将要执行字节码指令的地址,也就是0。

1、执行偏移地址为0的iconst指令,后面2是跟随的整形常量值2,执行后,2进入操作数栈栈顶。

2、执行偏移地址为1的istore指令,后面1是局部变量表的索引,取出栈顶元素2放入局部变量表索引为1的位置。

3、3、4步骤与1、2作用相同。

5、执行偏移地址为4的iload指令,后面1是局部变量表的索引,取出变量表索引为1的元素放入栈顶。

6、同5

7、执行偏移地址为6的iadd指令,取出操作栈栈顶的两个元素,进行相加并将结果放入栈顶。

8、执行偏移地址为7的istore指令,取出栈顶元素放入局部变量表索引为3的位置。

9、执行偏移地址为8的iload指令,取出局部变量表索引为3的元素放入栈顶。

10、执行偏移地址为9的ireturn指令,返回栈顶元素给方法调用者。

总结:

1、操作数栈只能通过出栈和入栈,不能通过索引访问。

2、栈元素类型与局部变量表一致,其中byte、short、char都会转为int类型,32位类型所占的栈容量为1,64位类型则占用栈容量为2。

3、是方法执行时变量值的临时存储区