字节码指令详解

x33g5p2x  于2021-12-18 转载在 其他  
字(5.2k)|赞(0)|评价(0)|浏览(371)

字节码指令详解

指令简介

在计算机中,CPU指令就是指挥机器工作的指令,程序就是一系列按一定顺序排列的指令,执行程序的过程就是执行指令的过程,也就是计算机的工作过程。

通常一条CPU指令包括两方面的内容:操作码和操作数,操作码表示要完成的操作,操作数表示参与运算的数据及其所在的单元地址(这个单元地址可以是寄存器、内存等)。

Java虚拟机中的字节码指令与CPU中指令类似,Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(operand)所构成。

操作数的数量以及长度取决于操作码,如果一个操作数的长度超过了一个字节,那么它将大端排序存储,即高位在前的字节序。

操作码只占一个字节,也就导致操作码个数不能超过256。

class文件只会出现数字形式的操作码,但是为了便于人识别,操作码有他对应的助记符形式。

对于基本数据类型,指令在设计的时候都用一个字母缩写来指代(boolean除外)。

基本数据类型缩写
byteb
shorts
inti
longl
floatf
doubled
charc
refrencea
boolean

指令详解

加载存储指令

加载存储指令用来交换局部变量表和操作数栈中的数据,以及将常量加载到操作数栈。

  1. 将一个局部变量从局部变量表中加载到操作数栈栈顶:
iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>

其中n为局部变量表中的slot的序号,double和long占用两个slot。

  1. 将一个操作数栈的栈顶元素存储到局部变量表:
istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>
  1. 将一个常量加载到操作数栈栈顶:
bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>

操作数为将要操作的数值或者常量池行号。

  1. 扩充局部变量表的访问索引的指令:wide。

运算指令

运算指令会取出操作数栈栈顶的两个元素进行某种特定的运算,然后将结果重新存入到操作数栈栈顶。

运算指令分为两种:整型运算的指令和浮点型运算的指令。

无论是哪种运算指令,都使用Java虚拟机的数据类型,由于没有直接支持byte、short、char和boolean类型的算术指令,使用操作int类型的指令代替。

加法指令:iadd、ladd、fadd、dadd

减法指令:isub、lsub、fsub、dsub

乘法指令:imul、lmul、fmul、dmul

除法指令:idiv、ldiv、fdiv、ddiv

求余指令:irem、lrem、frem、drem

取反指令:ineg、lneg、fneg、dneg

位移指令:ishl、ishr、iushr、lshl、lshr、lushr

按位或指令:ior、lor

按位与指令:iand、land

按位异或指令:ixor、lxor

局部变量自增指令:iinc

比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp

类型转换指令

类型转换指令可以将两种不同的数值类型进行相互转换。这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来解决字节码指令集不完备的问题。

宽化指令:

  • int类型到long、float或者double类型
  • long类型到float、double类型
  • float类型到double类型

处理窄化类型转换(Narrowing Numeric Conversions)时,必须显式地使用转换指令来完成,这些转换指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。

对象的创建与存取指令

new:创建对象。

getfield:获取实例对象的属性的值。

putfield:设置实例对象的属性的值。

getstatic:获取类的静态属性的值。

putstatic:设置类的静态属性的值。

数组的创建与存取指令

newarray:创建元素为基本数据类型的数组。

anewarray:创建数据类型为引用类型的数组。

multianewarray:创建多维数组。

把一个数组元素加载到操作数栈栈顶的指令:baload、caload、saload、iaload、laload、faload、daload、aaload

将一个操作数栈栈顶的值存储到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore

取数组长度的指令:arraylength

检查类实例类型的指令

instanceof、checkcast。

操作数栈管理指令

pop:将操作数栈的栈顶元素出栈。

pop2:将操作数栈的栈顶两个元素出栈。

复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2

将栈最顶端的两个数值互换:swap

控制转移指令

控制转移指令可以让Java虚拟机有条件或无条件地从指定的位置指令而不是控制转移指令的下一条指令继续执行程序。

控制转移指令如下:

  • 条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq 和 if_acmpne。
  • 复合条件分支:tableswitch、lookupswitch。
  • 无条件分支:goto、goto_w、jsr、jsr_w、ret。

方法调用指令

invokevirtual:指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。

invokeinterface:指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。

invokespecial:指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。

invokestatic:指令用于调用类方法(static方法)。

invokedynamic:指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面4条调用指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

方法调用指令与数据类型无关。

方法返回指令

方法返回指令是根据方法的返回值类型区分的,包括ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn 和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用。

异常处理指令

在Java程序中显式抛出异常的操作(throw语句)都由athrow指令来实现。

同步指令

monitorenter和monitorexit两条指令来支持synchronized关键字的语义。

常见语法结构对应的字节码指令

异常处理

public class SynchronizedDemo {

    final Object lock = new Object();

    void doLock() {
        synchronized (lock) {
            System.out.println("lock");
        }
    }
}

对应的字节码如下:

stack=2, locals=3, args_size=1
   0: aload_0
   1: getfield      #3                  // Field lock:Ljava/lang/Object;
   4: dup
   5: astore_1
   6: monitorenter
   7: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
  10: ldc           #5                  // String lock
  12: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  15: aload_1
  16: monitorexit
  17: goto          25
  20: astore_2
  21: aload_1
  22: monitorexit
  23: aload_2
  24: athrow
  25: return
Exception table:
   from    to  target type
       7    17    20   any
      20    23    20   any

在synchronized生成的字节码中,其中包含两条monitorexit指令,是为了保证所有的异常条件,都能够退出。

可以看到,编译后的字节码,带有一个叫Exception table的异常表,里面的每一行数据,都是一个异常处理:

  • from:指定字节码索引的开始位置
  • to:指定字节码索引的结束位置
  • targe:t异常处理的起始位置
  • type:异常类型

也就是说,只要在from和to之间发生了type异常,就会跳转到target所指定的位置。

装箱和拆箱

public class BoxDemo {

    public Integer cal() {
        Integer a = 1000;
        int b = a * 10;
        return b;
    }
}

对应的字节码如下:

0: sipush        1000
 3: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
 6: astore_1
 7: aload_1
 8: invokevirtual #3                  // Method java/lang/Integer.intValue:()I
11: bipush        10
13: imul
14: istore_2
15: iload_2
16: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
19: areturn

通过观察字节码,我们发现装箱和拆箱的本质:

  • 装箱:调用Xxx.valueOf()方法将基本数据类型包装成包装对象。
  • 拆箱:调用Xxx.xxxValue()方法将包装对象转为基本数据类型。

数组

package com.morris.jvm.bytecode;

public class ArrayDemo {
    int getValue() {
        int[] arr = new int[]{1111, 2222, 3333, 4444};
        return arr[2];
    }
    int getLength(int[] arr) {
        return arr.length;
    }
}

getValue()方法对应的字节码如下:

stack=4, locals=2, args_size=1
   0: iconst_4
   1: newarray       int
   3: dup
   4: iconst_0
   5: sipush        1111
   8: iastore
   9: dup
  10: iconst_1
  11: sipush        2222
  14: iastore
  15: dup
  16: iconst_2
  17: sipush        3333
  20: iastore
  21: dup
  22: iconst_3
  23: sipush        4444
  26: iastore
  27: astore_1
  28: aload_1
  29: iconst_2
  30: iaload
  31: ireturn
LineNumberTable:
  line 5: 0
  line 6: 28
LocalVariableTable:
  Start  Length  Slot  Name   Signature
      0      32     0  this   Lcom/morris/jvm/bytecode/ArrayDemo;
     28       4     1   arr   [I

可以看到,新建数组的代码,被编译成了newarray指令。

数组的创建具体操作:

  1. iconst_0:常量0压入操作数栈栈顶。
  2. sipush:将一个常量1111压入操作数栈栈顶。
  3. iastore:将栈顶int型数值存入数组的0索引位置。

数组元素的访问,是通过第28~30行字节码来实现的:

  1. aload_1:从局部变量表中第二个引用类型的局部变量压入操作数栈栈顶,这个引用就是生成的数组的引用。
  2. iconst_2:将int型2压入操作数栈栈顶。
  3. iaload:将int型数组指定索引的值压入操作数栈栈顶。

getLength()方法对应的字节码如下:

0: aload_1
1: arraylength
2: ireturn

获取数组的长度,是由字节码指令arraylength来完成的。

相关文章