likes
comments
collection
share

Java字节码操作数栈解析

作者站长头像
站长
· 阅读数 39

前言

前面文章我们说了Java字节码的结构,其中最重要的就是常量池和方法表内容,其中方法表内容涉及了各种JVM指令,如果不了解这些指令的话,将无法真正理解Java的方法是如何运行的。

正文

首先,我们得有个基本认识,在前面文章里也提及过,就是每当调用一个Java方法时,就会生成一个栈帧,方法的调用和返回就是栈帧的入栈和出栈。

而这个栈帧,也是分为2个部分,分别是操作数栈和本地变量区,其中本地变量区之前说过是一个数组,而这个操作数栈则揭秘了Java方法运行的本质,是利用栈来维持计算的。而各种指令也就是对这个栈以及本地变量数组进行操作,从而完成一些列复杂的逻辑。

操作数栈

首先这是一个栈,根据特性它只能FILO,在解释执行过程中,每当为Java方法分配栈帧时,JVM往往需要开辟一个额外空间作为操作数栈,用来存放计算的操作数以及返回结果。

这个很好理解,就是执行每一条指令前,JVM要求该指令的操作数已被压入操作数栈中,在执行指令时,JVM会将该指令需要的操作数弹出,并将指令的返回结果重新压入栈中

比如下图:

Java字节码操作数栈解析

我们只看操作数栈,这里栈顶元素是1和2,这时要执行加法iadd指令,会将1和2弹出栈,然后将求的值3再压入栈中:

Java字节码操作数栈解析

由于iadd指令只关心操作数栈前2个元素,所以对于栈中的?元素,不会做任何修改。

dup指令

既然先说道操作数栈,字节码中有好几个指令是直接作用在操作数栈上的,最常见的比如dup:复制栈顶元素,以及pop:舍弃栈顶元素。

这里我们来说一下dup指令,它可以直接复制栈顶元素,比如我们看下面代码:

public void foo(){
    Object obj = new Object();
}

然后其对应的JVM指令如下:

 public void foo();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class java/lang/Object
         3: dup
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         7: astore_1
         8: return
      LineNumberTable:
        line 6: 0
        line 7: 8

我们仅仅看Code区域,这里的new指令会产生未初始化的引用,比如上面第一行指令过后,会生成一个指向一块已分配、未初始化的内存引用压入到操作数栈中,然后需要以这个引用调用构造器,那么这个引用就会出栈,但是构造方法没有返回值再进行入栈,所以当new完之后,需要调用dup进行复制。

假如new完后引用R0入栈,dup后R1入栈,构造器拿着R1进行初始化,初始化完后,R0还在栈中,由于R1和R0指向同一块内存引用,所以R0指向的内存完成了初始化,而且栈顶元素是R0。

注意这里要dup的关键是构造器函数不会返回值。

pop指令

pop指令也是直接操作操作数栈的,它的作用是移除栈顶元素。

比如下面代码:

public static boolean bar(){
    return true;
}

public void foo(){
    bar();
}

我们在foo中调用bar,但是不使用bar的返回值,上述代码的JVM指令如下:

public void foo();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: invokestatic  #2                  // Method bar:()Z
         3: pop
         4: return
      LineNumberTable:
        line 10: 0
        line 11: 4

正常来说,当调用invokestatic指令后,会返回一个ture压入栈顶,但是我们不需要用这个返回值,所以可以调用pop指令把这个返回值再出栈。

注意pop和dup只能复制和出栈一个栈单元,对于long和double来说,需要使用pop2和dup2指令来复制和出栈2个栈单元。

加载常量到栈中的指令

加载常量到栈中的指令属于基础指令,也是非常常见的,这里分为2种:一种是直接加载常量到栈中,还有一种是加载常量池中的常量到栈中,指令如下:

Java字节码操作数栈解析

这里其实理解很好理解,xconst指令就是加载常用x类型的常量值到栈中,而bipush和sipush则是加载1个字节和2个字节能代表的int值,而ldc则是加载常量池中的常量。

比如下面代码:

public static final int a = 100;

public static final String s = "hello";

public void foo(){
    int b = 2;
    int c = a + b;
    System.out.println(s);
}

上面代码在foo中,我们定义了值为2的b,以及在外面定义了常量值,下面是JVM指令:

 public void foo();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_2
         1: istore_1
         2: bipush        100
         4: iload_1
         5: iadd
         6: istore_2
         7: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        10: ldc           #4                  // String hello
        12: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        15: return
      LineNumberTable:
        line 10: 0
        line 11: 2
        line 12: 7
        line 13: 15

不难发现 iconst_2就是把2入栈,bipush 100就是把100入栈,ldc #4 就是把常量池中的#4入栈,即 hello 入栈。

总结

本篇文章内容较为基础,但是十分重要,我们要明白JVM的指令集是基于栈来操作的,以及一些简单的指令来操作栈;而对于栈帧中另一个部分局部变量区,我们在下一篇文章继续解析,同时理解一些更复杂的指令集。

转载自:https://juejin.cn/post/7083438839301144612
评论
请登录