想要读懂 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
|
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: ......
|
接下来就是 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 -- 把栈顶的值复制一份再压回去,此时栈顶有两份一样的值,分别被 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
1 2 3 4 5 6 7 8 9 10 11
| for (int num : nums){ System.out.println(num); }
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