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

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

如何得到字节码?

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

1
2
3
4
package demo.a
public class B{
...
}

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

方法 1 、命令行

相关命令如下

1
2
3
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 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//
// 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);
}

}
}

阅读字节码

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

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

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

1
2
Compiled from "Hello.java"
public class Hello {

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

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

1
2
3
4
5
6
-- 字节码
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 就可以显示出来,如下所示

1
2
3
4
5
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 方法中干了什么吗

1
2
3
4
5
6
7
8
9
10
// 源码
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」加载到栈顶

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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 语句

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

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

1
2
3
4
5
6
7
-- 字节码
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

1
2
3
4
// 源码
List<Integer> nums = new ArrayList<>();
nums.add(1);
nums.add(2);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 初始化 List 对象
50: new #6 // class java/util/ArrayList
53: dup -- 把栈顶的值复制一份再压回去,此时栈顶有两份一样的值,分别被 5457 指令消耗了
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

1
2
3
4
5
6
7
8
9
10
11
// 源代码
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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-- 获取迭代器
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

1
2
3
4
// 源码
if (nums.size() == num2) {
System.out.println(num2);
}
1
2
3
4
5
6
7
8
9
10
    -- 如果 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