操作数栈(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、是方法执行时变量值的临时存储区