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