手把手教你读一个 Java 文件的字节码

想要读懂 Java 的字节码其实没那么难。当然,如果你有汇编语言的经验就会更好上手。本文手把手教你阅读一个简单 Java 文件的字节码。

如何得到字节码?

以下面这段示例代码为例,他存放在一个包中:

package demo.a
public class B{
    ...
}

通过下面这几个方法就可以查看代码的字节码:

方法 1 、命令行

相关命令如下

javac demo/a/B.java // 编译
jvavp -c demo.a.B   // 输出字节码
javap -c -verbose demo.a.B // 详细输出

方法 2 、idea 插件

下载个插件:「jclasslib Bytecode Viewer」,网址如下

https://plugins.jetbrains.com/plugin/9248-jclasslib-bytecode-viewer

安装该插件后,首先编译代码,然后
菜单 👉 「view」 👉 「Show Bytecode With jclasslib」
结果如下:

实验代码

我们使用下面这段代码,你可以将其输入 IDE 中

import java.util.ArrayList;
import java.util.List;

public class Hello {
    public static void main(String[] args) {
        int num1 = 1;
        int num2 = 130;
        int num3 = num1 + num2;
        int num4 = num2 - num1;
        int num5 = num1 * num2;
        int num6 = num2 / num1;

        final int num7 = 5;
        Integer num88 = 6;

        //看装箱指令
        if(num88 == 0){
            System.out.println(num1);
        }

        List<Integer> nums = new ArrayList<>();
        nums.add(1);
        nums.add(2);

        for (int num : nums){
            System.out.println(num);
        }

        if (nums.size() == num2) {
            System.out.println(num2);
        }
    }
}

下面是由 idea 反编译得到的代码,可以观察到 for 循环被改成了 while

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Hello {
    public Hello() {
    }

    public static void main(String[] args) {
        int num1 = 1;
        int num2 = 130;
        int var10000 = num1 + num2;
        var10000 = num2 - num1;
        var10000 = num1 * num2;
        var10000 = num2 / num1;
        int num7 = true;
        Integer num88 = 6;
        if (num88 == 0) {
            System.out.println(num1);
        }

        List<Integer> nums = new ArrayList();
        nums.add(1);
        nums.add(2);
        Iterator var10 = nums.iterator();

        while(var10.hasNext()) {
            int num = (Integer)var10.next();
            System.out.println(num);
        }

        if (nums.size() == num2) {
            System.out.println(num2);
        }

    }
}

阅读字节码

为了方便解释,我将字节码文件拆成小段,首先使用下面这个命令输出字节码

PS C:\Users\cedar\Desktop\ReadBytecode\code\target\classes> javap -c .\Hello.class

一开始就说明了这是「Hello.java」的字节码

Compiled from "Hello.java"
public class Hello {

紧接着自动创建了无参构造方法,调用了父类 Object 的初始化函数。 aload_0 是说把本地变亮表位置 0 的对象加载出来,而这个位置保存的是对自身的引用。

你会发现字节码每条命令前面也有一个数字,比如 0: aload_0 前面有一个 0 ,它代表 aload_0 这条指令在第 0 个位置。接着观察 4: return,它的位置怎么突然变成 4 了?那是因为 invokespecial 这个指令还有两个输入参数,一共占用三个字节

-- 字节码
 public Hello();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

1: invokespecial #1#1,代表常量池位置 1.常量池通过 javap -c -verbose demo.a.B 就可以显示出来,如下所示

Constant pool:
   #1 = Methodref          #15.#48        // java/lang/Object."<init>":()V
   #2 = Methodref          #12.#49        // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
   #3 = Methodref          #12.#50        // java/lang/Integer.intValue:()I
   ......

接下来就是 main 方法了,还记得我们在 main 方法中干了什么吗

// 源码
        int num1 = 1;
        int num2 = 130;
        int num3 = num1 + num2;
        int num4 = num2 - num1;
        int num5 = num1 * num2;
        int num6 = num2 / num1;

        final int num7 = 5;
        Integer num88 = 6;

它对应的字节码是下面这样的,具体内容我已经标注出来了,稍微解释一下 iconst_1 ,代表常量 int 1 ,也就是代码中有个常量 「1」加载到栈顶

  public static void main(java.lang.String[]);
    Code:

    -- 初始化 num1 = 1;保存到变量表 1
       0: iconst_1
       1: istore_1

    -- 初始化 num2 = 130; 保存到 变量表2,以下同理
       2: sipush        130
       5: istore_2

    -- 计算 num3(匿名了) = num1 + num2;
       6: iload_1
       7: iload_2
       8: iadd
       9: istore_3

    -- 计算 num4(匿名了) = num2 - num1;  
      10: iload_2
      11: iload_1
      12: isub
      13: istore        4

    -- 计算 num5(匿名了) = num1 * num2; 
      15: iload_1
      16: iload_2
      17: imul
      18: istore        5

    -- 计算 num6(匿名了) = num2 / num1;
      20: iload_2
      21: iload_1
      22: idiv
      23: istore        6

    -- final int num7 = 5;
      25: iconst_5
      26: istore        7

    -- Integer num88 = 6;
      28: bipush        6
      30: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      33: astore        8

然后是这个 if 语句

        if (num88 == 0) {
            System.out.println(num1);
        }

注意上文 num88 被保存到变量表位置 8,所以此处把位置 8 加载出来

-- 字节码
      35: aload         8
      37: invokevirtual #3                  // Method java/lang/Integer.intValue:()I
      40: ifne          50 -- 如果不等于 0 就跳转到 50
      43: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      46: iload_1          -- 存储 num1 的地方
      47: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V

然后我们操作了一个 List

// 源码
        List<Integer> nums = new ArrayList<>();
        nums.add(1);
        nums.add(2);
    -- 初始化 List 对象
      50: new           #6                  // class java/util/ArrayList
      53: dup              -- 把栈顶的值复制一份再压回去,此时栈顶有两份一样的值,分别被 54 和 57 指令消耗了
      54: invokespecial #7                  // Method java/util/ArrayList."<init>":()V
      57: astore        9 -- 将初始化的对象存到寄存器 9

    -- list -> add(1);
      59: aload         9
      61: iconst_1
      62: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      65: invokeinterface #8,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      70: pop           -- 丢弃了 add 返回值

    -- list -> add(2)
      71: aload         9
      73: iconst_2
      74: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      77: invokeinterface #8,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
      82: pop           -- 丢弃了 add 返回值

遍历 List ,这里 JVM 把 for 改成了 while

// 源代码
    for (int num : nums){
        System.out.println(num);
    }

//被 JVM 该成如下代码
    Iterator var10 = nums.iterator();
    while(var11.hasNext()) {
        int num = (Integer)var11.next();
        System.out.println(num);
    }
    -- 获取迭代器
      83: aload         9
      85: invokeinterface #9,  1            // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
      90: astore        10

    -- 
      92: aload         10
      94: invokeinterface #10,  1           // InterfaceMethod java/util/Iterator.hasNext:()Z
      99: ifeq          128 -- 如果等于 0,跳转到 128

    -- 获取 next() 并打印
     102: aload         10
     104: invokeinterface #11,  1           // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
     109: checkcast     #12                 // class java/lang/Integer  -- 检查对象是否为给定类型
     112: invokevirtual #3                  // Method java/lang/Integer.intValue:()I
     115: istore        11
     117: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
     120: iload         11
     122: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
     125: goto          92

最后我们写了个 if

// 源码
        if (nums.size() == num2) {
            System.out.println(num2);
        }
    -- 如果 list.size() == num2; 打印 num2
     128: aload         9
     130: invokeinterface #13,  1           // InterfaceMethod java/util/List.size:()I
     135: iload_2
     136: if_icmpne     146
     139: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
     142: iload_2
     143: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
     146: return
}

小结

Java 的字节码还是要比汇编简单一些。

这里再提一点,当要初始化一个 int 时(在 JVM 中:bool,byte,char,short 都是 int),根据不同的数字所占的位数不同,分别需要如下几个命令,方括号中给出了命令适用的范围

  • iconst: [-1, 5]
  • bipush: [-128, 127]
  • sipush: [-32768, 32767]
  • idc: any int value