带你阅读字节码
什么是字节码?
java bytecode 由单字节(byte)的指令组成,理论上最多支持 256 个操作码(opcode),实际上 Java 只使用了 200 个左右的操作码,还有一些操作码则保留给调试操作。
根据指令的性质,主要分为四大类:
- 栈操作指令,包括与局部变量交互的指令。
- 程序流程控制指令。
- 对象操作指令,包括方法调用指令。
- 算术运算以及类型转换指令。
一、如何生成字节码?
其实字节码就是 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、加载和存储指令
- 加载局部变量到操作栈:
iload
、iload_<n>
、lload
、lload_<n>
、fload
、fload_<n>
、dload
、dload_<n>
、aload
、aload_<n>
- 加载常量到操作栈:
bipush
、sipush
、ldc
、ldc_w
、ldc2_w
、aconst_null
、iconst_m1
、iconst_<i>
、lconst_<l>
、fconst_<f>
、dconst_<d>
- 将操作数栈存储到局部变量表:
istore
、istore_<n>
、lstore
、lstore_<n>
、fstore
、fstore_<n>
、dstore
、dstore_<n>
、astore
、astore_<n>
2、运算指令(运算结果会自动入栈)
- 加法指令:
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
3、类型转换
JVM 对类型宽化自然支持,并不需要执行指令,但是对类型窄化需要执行指令:i2b
、i2c
、i2s
、l2i
、f2i
、f2l
、d2i
、d2l
和d2f
。
4、对象的创建及访问
- 创建类实例:
new
- 访问类字段或实例字段:
getfield
、putfield
、getstatic
、putstatic
5、数组
- 创建数组:
newarray
、newwarray
、multianewarray
- 加载数组到操作数栈:
baload
、caload
、saload
、iaload
、laload
、faload
、daload
、aaload
- 将操作数栈存储到数组元素:
bastore
、castore
、sastore
、iastore
、fastore
、dastore
、aastore
- 取数组长度的指令:
arraylength
6、流程控制
- 条件判断:
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
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的位置