JVM指令分析实例五(操作数栈)

本篇为《JVM指令分析实例》的第五篇,相关实例均使用Oracle JDK 1.8编译,并使用javap生成字节码指令清单。

前几篇传送门:

JVM指令分析实例一(常量、局部变量、for循环)

JVM指令分析实例二(算术运算、常量池、控制结构)

JVM指令分析实例三(方法调用、类实例)

JVM指令分析实例四(数组、switch)

预备知识

局部变量表的变量槽(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高级特性与最佳实践》


个人公众号

二进制之路

JVM指令分析实例五(操作数栈)

相关推荐