Java字节码含义

Java字节码

操作数栈

在解释执行过程中,每当为Java方法分配栈桢时,Java虚拟机往往需要开辟一块额外的空间作为操作数栈,来存放计算的操作数以及返回结果。具体来说,JVM执行每一条指令之前,Java虚拟机要求该指令的操作数已被压入操作数栈中。在执行指令时,Java虚拟机会将该指令所需的操作数弹出,并且将指令的结果重新压入栈中。如下图,左侧绿色部分为局部变量区,右侧蓝色部分为操作数栈。

image

以加法指令iadd为例。假设在执行该指令前,栈顶的两个元素分别为int值1和int值2,那么iadd指令将弹出这两个int,并将求得的和int值3压入栈中。

image

由于iadd指令只消耗栈顶的两个元素,因此,对于离栈顶距离为2的元素,即图中的问号,iadd指令并不关心它是否存在,更加不会对其进行修改。

Java字节码中有好几条指令是直接作用在操作数栈上的。最为常见的便是:

  • dup:复制栈顶元素
  • pop:舍弃栈顶元素
  • swap: 交换栈顶两个元素

dup指令常用于复制new指令所生成的未经初始化的引用。例如在下面这段代码的foo方法中,当执行new指令时,Java虚拟机将指向一块已分配的、未初始化的内存的引用压入操作数栈中。

  public void foo(){
    Object o = new Object();
  }
  // 对应的字节码如下:
  public void foo();
    Code:
       0: new           #2                  // class java/lang/Object
       3: dup
       4: invokespecial #1                  // Method java/lang/Object."<init>":()V
       7: astore_1
       8: return

然后需要以这个引用为调用者,调用其构造器,也就是上面字节码中的invokespecial指令。要注意,该指令将消耗操作数栈上的元素,作为它的调用者以及参数(不过Object的构造器不需要参数)。因此,需要利用dup指令复制一份new指令的结果,并用来调用构造器。当调用返回之后,操作数栈上仍有原本由new指令生成的引用,可用于接下来的操作(即偏移量为7的字节码)。

pop指令则常用于舍弃调用指令的返回结果。例如在下面这段代码的foo方法中,调用静态方法bar,但是却不用其返回值。由于对应的invokestatic指令仍旧会将返回值压入foo方法的操作数栈中,如果返回为void,就不会将返回值压入操作数栈中了,因此Java虚拟机需要额外执行pop指令,将返回值舍弃。

  public void foo(){
    bar();
  }

  public static boolean bar(){
    return false;
  }
  // 对应的字节码如下:
  public void foo();
    Code:
       0: invokestatic  #2                  // Method bar:()Z
       3: pop
       4: return

注意上述两条指令只能处理非long或者非double类型的值,这是因为long类型或者double类型的值,需要占据两个栈单元。当遇到这些值时,我们需要同时复制栈顶两个单元的dup2指令,以及弹出栈顶两个单元的pop2指令

常数加载指令

在Java字节码中,有一部分指令可以直接将常量加载到操作数栈上。以int类型为例,Java虚拟机既可以通过iconst指令加载-1至5之间的int值,也可以通过bipush、sipush加载一个字节、两个字节所能代表的int值。Java虚拟机还可以通过ldc加载常量池中的常量值,例如ldc #18将加载常量池中的第18项。这些常量包括int类型、long类型、float类型、double类型、String类型以及Class类型的常量。具体如下表:

image

正常情况下,操作数栈的压入弹出都是一条条指令完成的。唯一的例外情况是在抛异常时,Java虚拟机会清除操作数栈上的所有内容,而后将异常实例压入操作数栈上。

局部变量区指令

Java方法栈桢的另外一个重要组成部分则是局部变量区,字节码程序可以将计算的结果缓存在局部变量区之中。实际上,Java虚拟机将局部变量区当成一个数组,依次存放this指针(仅非静态方法),所传入的参数,以及字节码中的局部变量。和操作数栈一样,long类型以及double类型的值将占据两个单元,其余类型仅占据一个单元。

public void foo(long l, float f) {
  {
    int i = 0;
  }
  {
    String s = "Hello, World";
  }
}

上面这段代码中的foo方法,由于它是一个实例方法,因此局部变量数组的第0个单元存放着this指针。第一个参数为long类型,于是数组的1、2两个单元存放着所传入的long类型参数的值。第二个参数则是float类型,于是数组的第3个单元存放着所传入的float类型参数的值。如下图。

image

在方法体里的两个代码块中,定义了两个局部变量i和s。由于这两个局部变量的生命周期没有重合之处,因此,Java编译器可以将它们编排至同一单元中。也就是说,局部变量数组的第4个单元将为i或者s。存储在局部变量区的值,通常需要加载至操作数栈中,方能进行计算,得到计算结果后再存储至局部变量数组中。这些加载、存储指令是区分类型的,这里加载是指从局部变量数组加载至操作数栈,而存储指令是指存储到局部变量数组。 如下图,为局部变量区访问指令表。

image

局部变量数组的加载、存储指令都需要指明所加载单元的下标。举例来说,aload 0指的是加载第0个单元所存储的引用,在前面示例中的foo方法里指的便是加载this指针。Java字节码中唯一能够直接作用于局部变量区的指令是iinc M N(M为非负整数,N为整数)。该指令指的是将局部变量数组的第M个单元中的int值增加N,常用于for循环中自增量的更新。

  public void foo() {
    for(int i = 100; i >= 0; i--){}
  }
  // 对应字节码
  public void foo();
    Code:
       0: bipush        100
       2: istore_1
       3: iload_1
       4: iflt          13
       7: iinc          1, -1
      10: goto          3
      13: return

数组相关指令

数组相关指令

  • newarray: 新建基本类型数组
  • anewarray: 新建引用类型数组
  • multianewarray: 生成多维数组
  • arraylength: 求数组长度

数组的加载指令以及存储指令,如下表

image

除返回指令外,其他的控制流指令均附带一个或者多个字节码偏移量,代表需要跳转到的位置。例如下面的abs方法中偏移量为1的条件跳转指令,当栈顶元素小于0时,跳转至偏移量为6的字节码。

public int abs(int i) {
    if (i >= 0) {
      return i;
    }
    return -i;
  }
  // 对应的字节码如下所示:
  public int abs(int i);
    0  iload_1 [i]
    1  iflt 6
    4  iload_1 [i]
    5  ireturn
    6  iload_1 [i]
    7  ineg
    8  ireturn

控制流指令

控制流指令,包括无条件跳转goto,条件跳转指令,tableswitch和lookupswtich(前者针对密集的cases,后者针对稀疏的cases),返回指令,以及被废弃的jsr,ret指令。其中返回指令是区分类型的。如下图

image

除返回指令外,其他的控制流指令均附带一个或者多个字节码偏移量,代表需要跳转到的位置。例如下面的abs方法中偏移量为1的条件跳转指令,当栈顶元素小于0时,跳转至偏移量为6的字节码。

public int abs(int i) {
    if (i >= 0) {
      return i;
    }
    return -i;
  }
  // 对应的字节码如下所示:
  public int abs(int i);
    0  iload_1 [i]
    1  iflt 6
    4  iload_1 [i]
    5  ireturn
    6  iload_1 [i]
    7  ineg
    8  ireturn

其他java字节码指令

Java相关指令,包括各类具备高层语义的字节码

  • new: 后跟目标类,生成该类的未初始化的对象
  • instanceof: 后跟目标类,判断栈顶元素是否为目标类/接口的实例。是则压入1,否则压入0
  • checkcast: 后跟目标类,判断栈顶元素是否为目标类/接口的实例。如果不是便抛出异常
  • athrow: 将栈顶异常抛出
  • monitorenter: 为栈顶对象加锁
  • monitorexit: 为栈顶对象解锁

此外,该类型的指令还包括字段访问指令,即静态字段访问指令getstatic、putstatic,和实例字段访问指令getfield、putfield。这四条指令均附带用以定位目标字段的信息,但所消耗的操作数栈元素皆不同。

image

以putfield为例,在上图中,它会把值v存储至对象obj的目标字段之中。

方法调用指令,包括invokestatic,invokespecial,invokevirtual,invokeinterface以及invokedynamic。除invokedynamic外,其他的方法调用指令所消耗的操作数栈元素是根据调用类型以及目标方法描述符来确定的。在进行方法调用之前,程序需要依次压入调用者(invokestatic不需要),以及各个参数。

public int neg(int i) {
    return -i;
  }
  public int foo(int i) {
    return neg(neg(i));
  }
  // foo方法对应的字节码如下:foo方法对应的字节码如下:
  public int foo(int i);
    0  aload_0 [this]
    1  aload_0 [this]
    2  iload_1 [i]
    3  invokevirtual FooTest.neg(int) : int [25]
    6  invokevirtual FooTest.neg(int) : int [25]
    9  ireturn

以上面这段代码为例,当调用foo(2)时,每条指令执行前后局部变量数组空间以及操作数栈的分布如下所示:

image

原文地址:https://www.cnblogs.com/dwtfukgv/p/15026851.html