JVM指令分析实例五(操作数栈)
本篇为《JVM指令分析实例》的第五篇,相关实例均使用Oracle JDK 1.8编译,并使用javap生成字节码指令清单。
前几篇传送门:
预备知识
局部变量表的变量槽(Variable Slot)
局部变量表的容量以变量槽(Variable Slot)为最小单位,虚拟机规范中并没有明确指明一个Slot应占用的内存空间大小。
每个Slot能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据。
reference类型表示对一个对象实例的引用,虚拟机规范没有说明它的长度及结构。
returnAddress类型目前已经很少见了,它是为字节码指令jsr、jsr_w和ret服务的,指向了一条字节码指令的地址。
对于64位的数据类型(long、double),虚拟机会以高位对齐的方式为其分配两个连续的Slot空间。
操作数栈管理指令
复制指令实例代码
package jvm.specification.se8.chapter3; public class NextIndex { private long index = 0; public long nextIndex() { return index++; } }
字节码指令序列
public long nextIndex(): 0: aload_0 // 将第1个局部变量this压入栈顶 1: dup // 复制栈顶this并压入栈顶. 栈底到栈顶:this、this 2: getfield #12 // Field index:J. 获取实例字段index并压入栈顶,消耗栈顶的1个this. 栈底到栈顶:this、index_for_ladd 5: dup2_x1 // 复制栈顶index数值,并插入第1个this下面. 栈底到栈顶:index_for_return、this、index_for_ladd 6: lconst_1 // 将long类型常量1压入栈顶 7: ladd // 将栈顶的2个long类型数值相加,并将结果压入栈顶. 栈底到栈顶:index_for_lreturn、this、index_for_putfield 8: putfield #12 // Field index:J. 将栈顶数值赋值给实例字段index 11: lreturn Constant pool: #1 = Class #2 // jvm/specification/se8/chapter3/NextIndex #2 = Utf8 jvm/specification/se8/chapter3/NextIndex #3 = Class #4 // java/lang/Object #4 = Utf8 java/lang/Object #5 = Utf8 index #6 = Utf8 J #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Methodref #3.#11 // java/lang/Object."<init>":()V #11 = NameAndType #7:#8 // "<init>":()V #12 = Fieldref #1.#13 // jvm/specification/se8/chapter3/NextIndex.index:J #13 = NameAndType #5:#6 // index:J
dup2_x1指令
复制栈顶的1个或2个值,并将其插入到栈顶的2个或3个值下面。
在预备知识中,我们对局部变量的Slot做了简单说明。可以简单理解为,long类型和double类型占2个Slot,其他类型占1个Slot。
下面拆解一下指令的定义(借用局部变量的Slot概念来描述,有点不太严谨,但易于理解与记忆。)。
复制栈顶的1个或2个值:
1个可以是long类型和double类型,2个是其他类型,共2个Slot。
插入到栈顶的2个或3个值下面:
如果复制的是1个值(即栈顶是long或double,共2个Slot),那么插入到栈顶的2个值(栈顶1个long或double,下面1个其他类型,共3个Slot)下面。
如果复制的是2个值(即栈顶是2个其他类型数值,共2个Slot),那么插入到栈顶的3个值(栈顶3个都是其他类型,共3个Slot)下面。
简单理解,dup2_x1指令的作用就是将栈顶的2个Slot的值复制并插入到栈顶的3个Slot的值下面。
对于本实例,执行dup2_x1指令之前的栈结构为(栈底到栈顶):this、index。由于index为long类型,占2个Slot。this为引用类型,占1个Slot。因此,dup2_x1指令将栈顶的2个Slot的index值复制并插入到栈顶的3个Slot的this引用下面。
操作数栈之指令系数法
dup总共有6个指令,分别是dup、dup_x1、dup_x2、dup2、dup2_x1和dup2_x2。初看这些指令,容易混淆而难以理解。经过分类和找规律,可以通过"指令系数法"来理解记忆,非常简单:
- 不带_x的指令是复制栈顶数据并压入栈顶。包括两个指令,dup和dup2
- 带_x的指令是复制栈顶数据并插入栈顶以下的某个位置。共有4个指令
- dup的系数代表要复制的Slot个数。
- dup开头的指令用于复制1个Slot的数据。例如1个int或1个reference类型数据
- dup2开头的指令用于复制2个Slot的数据。例如1个long,或2个int,或1个int+1个float类型数据
- 对于带_x的复制插入指令,只要将指令的dup和x的系数相加,结果即为需要插入的位置。因此
- dup_x1插入位置:1+1=2,即栈顶2个Slot下面。
- dup_x2插入位置:1+2=3,即栈顶3个Slot下面。
- dup2_x1插入位置:2+1=3,即栈顶3个Slot下面。
- dup2_x2插入位置:2+2=4,即栈顶4个Slot下面。
操作数栈管理指令共有9个,上面已经介绍了6个。剩下的3个用同样的方法就很容易理解了:
- pop:将栈顶的1个Slot数值出栈。例如1个short类型数值
- pop2:将栈顶的2个Slot数值出栈。例如1个double类型数值,或者2个int类型数值
- swap:交换栈顶的2个Slot数值位置。Java虚拟机没有提供交换两个64位数据类型(long、double)数值的指令。
备注:指令系数法是自己为了方便记忆起的名字
参考
《The Java Virtual Machine Specification, Java SE 8 Edition》
《Java虚拟机规范》(Java SE 8版)
《深入理解Java虚拟机 JVM高级特性与最佳实践》
个人公众号
二进制之路