带你阅读字节码

什么是字节码?

java bytecode 由单字节(byte)的指令组成,理论上最多支持 256 个操作码(opcode),实际上 Java 只使用了 200 个左右的操作码,还有一些操作码则保留给调试操作。

根据指令的性质,主要分为四大类:

  1. 栈操作指令,包括与局部变量交互的指令。
  2. 程序流程控制指令。
  3. 对象操作指令,包括方法调用指令。
  4. 算术运算以及类型转换指令。

一、如何生成字节码?

其实字节码就是 class 文件,如何生成 class 文件呢?就是使用 javac 命令。

# 生成字节码
javac -g:vars HelloByteCode.java

# 查看字节码
javap -verbose HelloByteCode

二、字节码指令

对于大部分为与数据类型相关的字节码指令,他们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:i代表对int类型的数据操作、l代表long、s代表short、b代表byte、c代表char、f代表float、d代表double、a代表reference。

1、加载和存储指令

  • 加载局部变量到操作栈:iloadiload_<n>lloadlload_<n>floadfload_<n>dloaddload_<n>aloadaload_<n>
  • 加载常量到操作栈:bipushsipushldcldc_wldc2_waconst_nulliconst_m1iconst_<i>lconst_<l>fconst_<f>dconst_<d>
  • 将操作数栈存储到局部变量表:istoreistore_<n>lstorelstore_<n>fstorefstore_<n>dstoredstore_<n>astoreastore_<n>

2、运算指令(运算结果会自动入栈)

  • 加法指令:iaddladdfadddadd
  • 减法指令:isublsubfsubdsub
  • 乘法指令:imullmulfmuldmul
  • 除法指令:idivldivfdivddiv
  • 求余指令:iremlremfremdrem
  • 取反指令:ineglnegfnegdneg
  • 位移指令:ishlishriushrlshllshrlushr
  • 按位或指令:iorlor
  • 按位与指令:iandland
  • 按位异或指令:ixorlxor
  • 局部变量自增指令:iinc
  • 比较指令:dcmpgdcmplfcmpgfcmpllcmp

3、类型转换

JVM 对类型宽化自然支持,并不需要执行指令,但是对类型窄化需要执行指令:i2bi2ci2sl2if2if2ld2id2ld2f

4、对象的创建及访问

  • 创建类实例:new
  • 访问类字段或实例字段:getfieldputfieldgetstaticputstatic

5、数组

  • 创建数组:newarraynewwarraymultianewarray
  • 加载数组到操作数栈:baloadcaloadsaloadialoadlaloadfaloaddaloadaaload
  • 将操作数栈存储到数组元素:bastorecastoresastoreiastorefastoredastoreaastore
  • 取数组长度的指令:arraylength

6、流程控制

  • 条件判断:ifeqifltifleifneifgtifgeifnullifnonnullif_icmpeqif_icmpneif_icmplt, if_icmpgtif_icmpleif_icmpgeif_acmpeqif_acmpne
  • 复合条件分支:tableswitchlookupswitch
  • 无条件分支:gotogoto_wjsrjsr_wret

7、方法调用和返回指令(调用之后数据依然在操作数栈中)

  • invokevirtual:用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。
  • invokeinterface:用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
  • invokespecial:用于调用一些需要特殊处理的实例方法,包括实例初始化方法(§2.9)、私有方法和父类方法。
  • invokestatic:指令用于调用类方法(static方法)。

8、返回值指令

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

三、阅读字节码文件

阅读字节码文件之前,先了解一下 JVM 有哪些常用的操作码(opcode),官方文档

例子:

public class HelloByteCode {
    public static void main(String[] args) {
        System.out.println("Hello ByteCode.");
		
        // 基本类型定义
        byte b1 = 100;
        short s1 = 30000;
        int i1 = 50;
        int i2 = 200;
        int i3 = 40000;
        long l1 = 300;
        long l2 = 1000000;
        float f1 = 10.5F;
        double d1 = 20.5D;
        // 引用类型定义
        String str1 = "Hello";

        // 四则运算
        int sum = i1 + i2;
        int sub = i1 - i2;
        int mul = i1 * i2;
        int dvi = i2 / i1;

        if (sum == 0) {
            System.out.println("sum == 0");
        }

        for (int i = 0; i < 3; i++) {
            System.out.println("index:" + i);
        }

    }

}

编译、查看、翻译字节码:

➜  java javac -g:vars com/snailwu/course/code/HelloByteCode.java
➜  java javap -verbose com/snailwu/course/code/HelloByteCode
// class 文件的位置
Classfile /Users/wu/GitLab/java-course-code/src/main/java/com/snailwu/course/code/HelloByteCode.class
  // 最后修改时间,文件大小
  Last modified 2021-6-28; size 1243 bytes
  // MD5
  MD5 checksum 944f6523f8b0126fb7d76753c9193800
// 全类名
public class com.snailwu.course.code.HelloByteCode
  // 最低最高版本号
  minor version: 0
  major version: 52
  // 类的修饰符
  flags: ACC_PUBLIC, ACC_SUPER
// 常量池
Constant pool:
   #1 = Methodref          #22.#58        // java/lang/Object."<init>":()V
   #2 = Fieldref           #59.#60        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #61            // Hello ByteCode.
   #4 = Methodref          #62.#63        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Integer            40000
   #6 = Long               300l
   #8 = Long               1000000l
  #10 = Float              10.5f
  #11 = Double             20.5d
  #13 = String             #64            // Hello
  #14 = String             #65            // sum == 0
  #15 = Class              #66            // java/lang/StringBuilder
  #16 = Methodref          #15.#58        // java/lang/StringBuilder."<init>":()V
  #17 = String             #67            // index:
  #18 = Methodref          #15.#68        // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #19 = Methodref          #15.#69        // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  #20 = Methodref          #15.#70        // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #21 = Class              #71            // com/snailwu/course/code/HelloByteCode
  #22 = Class              #72            // java/lang/Object
  #23 = Utf8               <init>
  #24 = Utf8               ()V
  #25 = Utf8               Code
  #26 = Utf8               LocalVariableTable
  #27 = Utf8               this
  #28 = Utf8               Lcom/snailwu/course/code/HelloByteCode;
  #29 = Utf8               main
  #30 = Utf8               ([Ljava/lang/String;)V
  #31 = Utf8               i
  #32 = Utf8               I
  #33 = Utf8               args
  #34 = Utf8               [Ljava/lang/String;
  #35 = Utf8               b1
  #36 = Utf8               B
  #37 = Utf8               s1
  #38 = Utf8               S
  #39 = Utf8               i1
  #40 = Utf8               i2
  #41 = Utf8               i3
  #42 = Utf8               l1
  #43 = Utf8               J
  #44 = Utf8               l2
  #45 = Utf8               f1
  #46 = Utf8               F
  #47 = Utf8               d1
  #48 = Utf8               D
  #49 = Utf8               str1
  #50 = Utf8               Ljava/lang/String;
  #51 = Utf8               sum
  #52 = Utf8               sub
  #53 = Utf8               mul
  #54 = Utf8               dvi
  #55 = Utf8               StackMapTable
  #56 = Class              #34            // "[Ljava/lang/String;"
  #57 = Class              #73            // java/lang/String
  #58 = NameAndType        #23:#24        // "<init>":()V
  #59 = Class              #74            // java/lang/System
  #60 = NameAndType        #75:#76        // out:Ljava/io/PrintStream;
  #61 = Utf8               Hello ByteCode.
  #62 = Class              #77            // java/io/PrintStream
  #63 = NameAndType        #78:#79        // println:(Ljava/lang/String;)V
  #64 = Utf8               Hello
  #65 = Utf8               sum == 0
  #66 = Utf8               java/lang/StringBuilder
  #67 = Utf8               index:
  #68 = NameAndType        #80:#81        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #69 = NameAndType        #80:#82        // append:(I)Ljava/lang/StringBuilder;
  #70 = NameAndType        #83:#84        // toString:()Ljava/lang/String;
  #71 = Utf8               com/snailwu/course/code/HelloByteCode
  #72 = Utf8               java/lang/Object
  #73 = Utf8               java/lang/String
  #74 = Utf8               java/lang/System
  #75 = Utf8               out
  #76 = Utf8               Ljava/io/PrintStream;
  #77 = Utf8               java/io/PrintStream
  #78 = Utf8               println
  #79 = Utf8               (Ljava/lang/String;)V
  #80 = Utf8               append
  #81 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #82 = Utf8               (I)Ljava/lang/StringBuilder;
  #83 = Utf8               toString
  #84 = Utf8               ()Ljava/lang/String;
{
  // 空的构造方法
  public com.snailwu.course.code.HelloByteCode();
    // 方法的修饰符:包括参数及返回值。
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      // 为什么args_size为1?非静态方法默认第一个参数为 this 对象。参看 LocalVariableTable
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/snailwu/course/code/HelloByteCode;

  // main 方法
  public static void main(java.lang.String[]);
    // 方法的修饰符:包括参数及返回值。
    descriptor: ([Ljava/lang/String;)V
    // 修饰符
    flags: ACC_PUBLIC, ACC_STATIC
    // 代码区
    Code:
      // 第一个参数是 args
      stack=3, locals=19, args_size=1
        // 获取 PrintStream 实例
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        // 将常量加载到操作数栈
         3: ldc           #3                  // String Hello ByteCode.
        // 调用方法进行输出
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        // 将 100 压入操作数栈
         8: bipush        100
        // 将操作数栈栈顶元素存到本地变量slot为1的位置
        10: istore_1
        // 将 30000 压栈
        11: sipush        30000
        // 将栈顶元素存到slot为2的位置
        14: istore_2
        // 将 50 压栈
        15: bipush        50
        // 将栈顶元素存到slot为3的位置
        17: istore_3
        // 将 200 压栈
        18: sipush        200
        // 将栈顶元素存到slot为4的位置
        21: istore        4
        // 将 40000 压栈
        23: ldc           #5                  // int 40000
        // 将栈顶元素存到slot为5的位置
        25: istore        5
        // 将 300 压栈,long占用两个slot
        27: ldc2_w        #6                  // long 300l
        // 将栈顶元素存到slot为6、7的位置
        30: lstore        6
        // 将 1000000 压栈,long占用两个slot
        32: ldc2_w        #8                  // long 1000000l
        // 将栈顶元素存到slot为8、9的位置
        35: lstore        8
        // 将 10.5 压栈
        37: ldc           #10                 // float 10.5f
        // 将栈顶元素存到slot为10的位置
        39: fstore        10
        // 将 20.5 压栈
        41: ldc2_w        #11                 // double 20.5d
        // 将栈顶元素存到slot为11、12的位置
        44: dstore        11
        // 将 Hello 压栈
        46: ldc           #13                 // String Hello
        / 将栈顶元素存到slot为13的位置
        48: astore        13
        // 加载slot为3处的值到栈顶
        50: iload_3
        // 加载slot为4处的值到栈顶
        51: iload         4
        // 将栈顶两个元素弹出,进行相加,然后结果存入栈顶
        53: iadd
        // 将栈顶元素存入slot为14的位置
        54: istore        14
        // 加载slot为3处的值到栈顶
        56: iload_3
        // 加载slot为4处的值到栈顶
        57: iload         4
        // 将栈顶两个元素弹出,进行相减,然后结果存入栈顶
        59: isub
        // 将栈顶元素存入slot为15的位置
        60: istore        15
        // 加载slot为3处的值到栈顶
        62: iload_3
        // 加载slot为4处的值到栈顶
        63: iload         4
        // 将栈顶两个元素弹出,进行相乘,然后结果存入栈顶
        65: imul
        // 将栈顶元素存入slot为16的位置
        66: istore        16
        // 加载slot为4处的值到栈顶
        68: iload         4
        // 加载slot为3处的值到栈顶
        70: iload_3
        // 将栈顶两个元素弹出,进行相除,然后结果存入栈顶
        71: idiv
        // 将栈顶元素存入slot为17的位置
        72: istore        17
        // 加载slot为14处的值到栈顶
        74: iload         14
        // 如果栈顶元素不等于0,跳转到偏移量为87的位置往下执行
        76: ifne          87
        // 如果栈顶元素等于0,执行76到87之间的代码
        // 79到84:输出 "sum == 0"
        79: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        82: ldc           #14                 // String sum == 0
        84: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        // 将 0 压栈
        87: iconst_0
        // 将栈顶元素存入slot为18的位置
        88: istore        18
        // 加载slot为18处的值到栈顶
        90: iload         18
        // 将 3 压入栈顶
        92: iconst_3
        // 弹出栈顶两个元素进行比较,如果 val1(第一个弹出的元素:3) >= val2(第二个弹出的元素:0) 则跳转到偏移量为128的位置往下执行
        93: if_icmpge     128
        // 获取 PrintStream 实例
        96: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        // new 关键字,新建对象 StringBuilder,并将引用压入栈顶
        99: new           #15                 // class java/lang/StringBuilder
        // 复制栈顶的元素并压入栈顶,就是把 StringBuilder 的引用复制一份,invokespecial 会消耗一个 引用,如果不复制,后面的 append 就没有引用可以使用了
       102: dup
       // 执行 StringBuilder 的初始化方法,消耗栈顶的一个 StringBuilder 对象引用,返回值是 void,所以这个引用消耗了就没了
       103: invokespecial #16                 // Method java/lang/StringBuilder."<init>":()V
       // 将 "index" 压入栈顶
       106: ldc           #17                 // String index:
       // 弹出栈顶的 StringBuilder 引用执行 append 方法,执行结果(返回值:StringBuilder类型的)压入栈顶
       108: invokevirtual #18                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
       // 将slot为18处的值加载到栈顶
       111: iload         18
       // 弹出栈顶的 StringBuilder 引用执行 append 方法,执行结果(返回值:StringBuilder类型的)压入栈顶
       113: invokevirtual #19                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
       // 弹出栈顶的 StringBuilder 引用执行 String 方法,执行结果(返回值:String类型的)压入栈顶
       116: invokevirtual #20                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
       // 弹出栈顶元素,执行 println 方法
       119: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       // slot为18出的值自增1
       122: iinc          18, 1
       // 跳转到偏移量为90的位置继续执行
       125: goto          90
       // 执行返回
       128: return
      // 本地变量表
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           90      38    18     i   I
            0     129     0  args   [Ljava/lang/String;
           11     118     1    b1   B
           15     114     2    s1   S
           18     111     3    i1   I
           23     106     4    i2   I
           27     102     5    i3   I
           32      97     6    l1   J
           37      92     8    l2   J
           41      88    10    f1   F
           46      83    11    d1   D
           50      79    13  str1   Ljava/lang/String;
           56      73    14   sum   I
           62      67    15   sub   I
           68      61    16   mul   I
           74      55    17   dvi   I
      // 栈表
      StackMapTable: number_of_entries = 3
        frame_type = 255 /* full_frame */
          offset_delta = 87
          locals = [ class "[Ljava/lang/String;", int, int, int, int, int, long, long, float, double, class java/lang/String, int, int, int, int ]
          stack = []
        frame_type = 252 /* append */
          offset_delta = 2
          locals = [ int ]
        frame_type = 250 /* chop */
          offset_delta = 37
}

是不是字节码文件也很容易阅读。

四、指令总结

每一个线程都有一个保存帧的栈。在每一个方法调用的时候创建一个帧。一个帧包括了三个部分:操作栈,局部变量数组,和一个对当前方法所属类的常量池的引用。

局部变量数组也被称之为局部变量表,它包含了方法的参数,也用于保存一些局部变量的值。参数值得存放总是在局部变量数组的index0开始的。如果当前帧是由构造函数或者实例方法创建的,那么该对象引用将会存放在location0处,然后才开始存放其余的参数。

局部变量表的大小由编译时决定,同时也依赖于局部变量的数量和一些方法的大小。操作栈是一个(LIFO)栈,用于压入和取出值,其大小也在编译时决定。某些opcode指令将值压入操作栈,其余的opcode指令将操作数取出栈。使用它们后再把结果压入栈。操作栈也用于接收从方法中返回的值。