1、字节码指令简介
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需的参数(称为操作数,Operand)构成。由于Java虚拟机采用面向操作数栈而不是面向寄存器的架构,所以大多数指令都不包含操作数,只有一个操作码,指令参数都存放在操作数栈中。
2、字节码与数据类型
- 在Java虚拟机的指令集中,大多数指令都包含其操作所对应的数据类型信息。举个例子,iload指令用于从局部变量表中加载int型的数据到操作数栈中,而fload指令加载的则是float类型的数据。这两条指令的操作在虚拟机内部可能会是由同一段代码来实现的,但在Class文件中它们必须拥有各自独立的操作码。
- 对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:i代表对int类型的数据操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。也有一些指令的助记符中没有明确指明操作类型的字母,例如arraylength指令,它没有代表数据类型的特殊字符,但操作数永远只能是一个数组类型的对象。还有另外一些指令,例如无条件跳转指令goto则是与数据类型无关的指令。
3、加载和存储指令
3.1、常量入栈指令
操作码 | 操作数 | 描述(操作数栈) | 对应代码 |
---|---|---|---|
aconst_null | null值入栈 | Object a = null; | |
iconst_<n> | n可以为1-5表示值n(int)入栈,也可以为m1表示-1(int)入栈 | int a = 1; | |
lconst_<n> | n可以为0,1表示值n(long)入栈 | long a = 0; | |
fconst_<n> | n可以为0-2表示值n(float)入栈 | float a = 0; | |
dconst_<n> | n可以为0、1表示值n(double)入栈 | dobule a = 0; | |
bipush | valuebyte | valuebyte值带符号扩展成int值入栈。 此时int 值的范围是 -128 ~ 127 | int a = 10; |
sipush | valuebyte1 valuebyte2 | (valuebyte1 << 8) | valuebyte2 值带符号扩展成int值入栈。 此时int 值的范围是 -32768~32767 | int a = 1000; |
ldc | indexbyte1 | 常量池中的常量值(int, float, string reference, object reference)入栈。 | int a = Integer.MAX_VALUE; |
ldc_w | indexbyte1 indexbyte2 | ldc和ldc_w指令用于访问运行时常量池中除了double和long类型的值(但是包括String类型实例)。当使用的运行时常量池的项目过多时(多余256个,一个字节能表示的范围),需要使用ldc_w来代替。 | |
ldc2_w | indexbyte1 indexbyte2 | 常量池中常量(long, double)入栈。 | double a = Double.MAX_VALUE; |
3.2、局部变量值转移到栈指令
操作码 | 操作数 | 描述(操作数栈) | 对应代码 |
---|---|---|---|
aload_<n> | 从局部变量n中装载 引用类型 值入栈。n的取值为0-3 | 使用局部变量的前四个参数(且使用的类型是引用类型) | |
aload | indexbyte | 从局部变量indexbyte中装载 引用类型 值入栈。 | 使用局部变量的后几个参数(且使用的类型是引用类型) |
iload_<n> | 从局部变量n中装载 int 值入栈。n的取值为0-3 | 使用局部变量的前四个参数(且使用的类型是int类型) | |
iload | indexbyte | 从局部变量indexbyte中装载 int 值入栈。 | 使用局部变量的后几个参数(且使用的类型是int类型) |
fload_<n> | 从局部变量n中装载 float 入栈。n的取值为0-3 | 使用局部变量的前四个参数(且使用的类型是float类型) | |
fload | indexbyte | 从局部变量indexbyte中装载 float 值入栈。 | 使用局部变量的后几个参数(且使用的类型是float类型) |
dload_<n> | 从局部变量n中装载 double 入栈。n的取值为0-3 | 使用局部变量的前四个参数(且使用的类型是double类型) | |
dload | indexbyte | 从局部变量indexbyte中装载 double 值入栈。 | 使用局部变量的后几个参数(且使用的类型是double类型) |
aaload | 从引用类型数组中装载指定项的值。 | 使用局部变量的参数(且使用的类型是引用类型数组) | |
iaload | 从int类型数组中装载指定项的值。 | 使用局部变量的参数(且使用的类型是int类型数组) | |
laload | 从long类型数组中装载指定项的值。 | ||
faload | 从float类型数组中装载指定项的值。 | ||
daload | 从double类型数组中装载指定项的值。 | ||
baload | 从boolean类型数组或byte类型数组中装载指定项的值(先转换为int类型值,后压栈)。 | ||
caload | 从char类型数组中装载指定项的值(先转换为int类型值,后压栈)。 | ||
saload | 从short类型数组中装载指定项的值(先转换为int类型值,后压栈)。 |
3.3、栈顶值保存到局部变量表
操作码 | 操作数 | 描述(操作数栈) |
---|---|---|
astore | indexbyte | 将栈顶引用类型值保存到局部变量indexbyte中。 |
astore_<n> | 将栈顶引用类型值保存到局部变量n中。n取值为0-3 | |
istore | indexbyte | 将栈顶int类型值保存到局部变量indexbyte中。 |
istore_<n> | 将栈顶int类型值保存到局部变量n中。n取值为0-3 | |
(wide)lstore | indexbyte | 将栈顶long类型值保存到局部变量indexbyte中。 |
lstroe_<n> | 将栈顶long类型值保存到局部变量n中。n取值为0-3 | |
(wide)fstore | indexbyte | 将栈顶float类型值保存到局部变量indexbyte中。 |
fstore_<n> | 将栈顶float类型值保存到局部变量n中。n取值为0-3 | |
(wide)dstore | indexbyte | 将栈顶double类型值保存到局部变量indexbyte中。 |
dstore_<n> | 将栈顶double类型值保存到局部变量n中。n取值为0-3 | |
aastore | 将栈顶引用类型值保存到指定引用类型数组的指定项。 | |
iastore | 将栈顶int类型值保存到指定int类型数组的指定项。 | |
lastore | 将栈顶long类型值保存到指定long类型数组的指定项。 | |
fastore | 将栈顶float类型值保存到指定float类型数组的指定项。 | |
dastore | 将栈顶double类型值保存到指定double类型数组的指定项。 | |
bastroe | 将栈顶boolean类型值或byte类型值保存到指定boolean类型数组或byte类型数组的指定项。 | |
castore | 将栈顶char类型值保存到指定char类型数组的指定项。 | |
sastore | 将栈顶short类型值保存到指定short类型数组的指定项。 |
3.4、对象操作指令
操作码 | 操作数 | 描述(操作数栈) |
---|---|---|
new | indexbyte1 indexbyte2 | 创建新的对象实例。 |
checkcast | indexbyte1 | indexbyte |
instanceof | indexbyte1 indexbyte2 | 判断类型。 |
getfield | indexbyte1 indexbyte2 | 获取对象字段的值。 |
putfield | indexbyte1 indexbyte2 | 给对象字段赋值。 |
getstatic | indexbyte1 indexbyte2 | 获取静态字段的值。 |
putstatic | indexbyte1 indexbyte2 | 给静态字段赋值。 |
3.5、方法调用指令
操作码 | 操作数 | 描述(操作数栈) |
---|---|---|
invokespecial | indexbyte1 indexbyte2 | 编译时方法绑定调用方法。 |
invokevirtual | indexbyte1 indexbyte2 | 运行时方法绑定调用方法。 |
invokestatic | indexbyte1 indexbyte2 | 调用静态方法。 |
invokeinterface | indexbyte1 indexbyte2 count 0 | 调用接口方法。 |
3.6、线程同步指令
操作码 | 操作数 | 描述(操作数栈) |
---|---|---|
monitorenter | 进入并获得对象监视器。 | |
monitorexit | 释放并退出对象监视器。 |
3.7、运算指令
算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。大体上运算指令可以分为两种:对整型数据进行运算的指令与对浮点型数据进行运算的指令。整数与浮点数的算术指令在溢出和被零除的时候也有各自不同的行为表现。无论是哪种算术指令,均是使用Java虚拟机的算术类型来进行计算的,换句话说是不存在直接支持byte、short、char和boolean类型的算术指令,对于上述几种数据的运算,应使用操作int类型的指令代替。所有的算术指令包括:
- 加法指令: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
4、字节码文件解读
4.1、源java代码
public class ByteCodeTest {
private static final String X = "xxx";
public String test(String a) {
return X + a;
}
}
4.2、编译之后的class文件
public class ByteCodeTest {
private static final String X = "xxx";
public ByteCodeTest() {
}
public String test(String a) {
return "xxx" + a;
}
}
4.3、使用javap命令计算字节码指令
使用以下命令
javap -verbose -p ByteCodeTest.class
结果代码清单如下
Classfile /Users/huxiaolong/Desktop/person/sync/target/classes/ByteCodeTest.class
Last modified 2022-11-5; size 610 bytes
MD5 checksum 299ef8b31b427e36fb1f58c12c155f32
Compiled from "ByteCodeTest.java"
public class ByteCodeTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #8.#24 // java/lang/Object."<init>":()V
#2 = Class #25 // java/lang/StringBuilder
#3 = Methodref #2.#24 // java/lang/StringBuilder."<init>":()V
#4 = Class #26 // ByteCodeTest
#5 = String #27 // xxx
#6 = Methodref #2.#28 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#7 = Methodref #2.#29 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#8 = Class #30 // java/lang/Object
#9 = Utf8 X
#10 = Utf8 Ljava/lang/String;
#11 = Utf8 ConstantValue
#12 = Utf8 <init>
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 LocalVariableTable
#17 = Utf8 this
#18 = Utf8 LByteCodeTest;
#19 = Utf8 test
#20 = Utf8 (Ljava/lang/String;)Ljava/lang/String;
#21 = Utf8 a
#22 = Utf8 SourceFile
#23 = Utf8 ByteCodeTest.java
#24 = NameAndType #12:#13 // "<init>":()V
#25 = Utf8 java/lang/StringBuilder
#26 = Utf8 ByteCodeTest
#27 = Utf8 xxx
#28 = NameAndType #31:#32 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#29 = NameAndType #33:#34 // toString:()Ljava/lang/String;
#30 = Utf8 java/lang/Object
#31 = Utf8 append
#32 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#33 = Utf8 toString
#34 = Utf8 ()Ljava/lang/String;
{
private static final java.lang.String X;
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL
ConstantValue: String xxx
public ByteCodeTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 11: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LByteCodeTest;
public java.lang.String test(java.lang.String);
descriptor: (Ljava/lang/String;)Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: ldc #5 // String xxx
9: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
12: aload_1
13: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
16: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
19: areturn
LineNumberTable:
line 16: 0
LocalVariableTable:
Start Length Slot Name Signature
0 20 0 this LByteCodeTest;
0 20 1 a Ljava/lang/String;
}
SourceFile: "ByteCodeTest.java"
4.3.1 类的基本信息
Classfile /Users/huxiaolong/Desktop/person/sync/target/classes/ByteCodeTest.class
Last modified 2022-11-5; size 610 bytes
MD5 checksum 299ef8b31b427e36fb1f58c12c155f32
Compiled from "ByteCodeTest.java"
public class ByteCodeTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
前三行描述了.class文件的所在路径、最后修改时间、校验和。
后三行描述了类的基本信息。
- minor:此版本号
- major:主版本号
- flags:描述了类的相关修饰符,ACC_PUBLIC表示类是public公开类型,flag可以有以下取值:
4.3.2 常量池
Constant pool:
#1 = Methodref #8.#24 // java/lang/Object."<init>":()V
#2 = Class #25 // java/lang/StringBuilder
#3 = Methodref #2.#24 // java/lang/StringBuilder."<init>":()V
#4 = Class #26 // ByteCodeTest
#5 = String #27 // xxx
#6 = Methodref #2.#28 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#7 = Methodref #2.#29 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#8 = Class #30 // java/lang/Object
#9 = Utf8 X
#10 = Utf8 Ljava/lang/String;
#11 = Utf8 ConstantValue
#12 = Utf8 <init>
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 LocalVariableTable
#17 = Utf8 this
#18 = Utf8 LByteCodeTest;
#19 = Utf8 test
#20 = Utf8 (Ljava/lang/String;)Ljava/lang/String;
#21 = Utf8 a
#22 = Utf8 SourceFile
#23 = Utf8 ByteCodeTest.java
#24 = NameAndType #12:#13 // "<init>":()V
#25 = Utf8 java/lang/StringBuilder
#26 = Utf8 ByteCodeTest
#27 = Utf8 xxx
#28 = NameAndType #31:#32 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#29 = NameAndType #33:#34 // toString:()Ljava/lang/String;
#30 = Utf8 java/lang/Object
#31 = Utf8 append
#32 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#33 = Utf8 toString
#34 = Utf8 ()Ljava/lang/String;
这一部分就是类中使用到的常量,每个常量都有编号,以#号开头,font color=“#660000”>=号后面就是常量类型,类型后就是常量的值。每个常量都由三部分组成——编号、类型、常量值组成。
例如#1常量的类型是Methodref,表示这个常量描述的是方法相关的信息;#4变量的类型是Class,表示这个常量描述的是类相关的信息。
注意:一个常量还可以引用其他常量,例如#1就引用了#8与#24常量的值。而最终#1得到的值就是 java/lang/Object.“”:\()V
4.3.3、包含方法与类的私用量
{
// X 静态常量
private static final java.lang.String X;
// 描述 包括访问权限 类型
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL
ConstantValue: String xxx
// 。。。。
public java.lang.String test(java.lang.String);
// descriptor:精简的描述了方法的入参和出参。
descriptor: (Ljava/lang/String;)Ljava/lang/String;
flags: ACC_PUBLIC
// 方法的代码
Code:
// locals 是该方法中的本地变量有多少个
// stack 是方法在执行过程中,操作数栈中最大深度
// args_size 是参数数量
stack=2, locals=2, args_size=2
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: ldc #5 // String xxx
9: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
12: aload_1
13: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
16: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
19: areturn
// 代码对照表 line 16: 0为例,表示源.java文件的第16行代码,对应的是0偏移量
LineNumberTable:
line 16: 0
// 本地变量表
LocalVariableTable:
// start:为这个变量可见的起始偏移位置,它的值必须是在 Code 中存在的偏移量值。(也就是这个变量第一次出现在哪个偏移量)
// length:为该变量的有效长度,描述的是该变量在方法中的有效范围大小。
// slot:为变量在 local variable(本地变量表) 中的位置,这可以帮助我们在指令中确定对应的变量。
// Name 则是变量名
// Signature 为该变量的类型
Start Length Slot Name Signature
0 20 0 this LByteCodeTest;
0 20 1 a Ljava/lang/String;
// ....
}