欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-06-24 18:19:47
...

本篇为《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高级特性与最佳实践》

 


 

转载请注明来源:http://zhanjia.iteye.com/blog/2432142

 

个人公众号

二进制之路

 

JVM指令分析实例五(操作数栈)
            
    
    博客分类: JavaJava虚拟机(JVM) JVM虚拟机操作数栈stack字节码