Java字节码指令集
字节码指令集结构
Java 虚拟机(JVM)的指令由一个字节长度的、代表着某种特定操作含义的操作码(Opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(Operands)所构成。
对于大部分与数据类型相关的字节码指令,其操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:
i:代表对int类型的数据操作l:代表longs:代表shortb:代表bytec:代表charf:代表floatd:代表doublea:代表reference(引用类型)
指令集分类
加载和存储指令
- 将一个局部变量加载到操作栈的指令:
iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n> - 将一个数值从操作数栈存储到局部变量表的指令:
istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n> - 将一个常量加载到操作数栈的指令:
bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d> - 扩充局部变量表的访问索引的指令:
wide
运算指令
- 加法指令:
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
类型转换指令
Java 虚拟机对于宽化类型转换(Widening Conversion)直接支持,并不需要指令执行,包括:
int类型到long、float或者double类型long类型到float、double类型float类型到double类型
窄化类型转换(Narrowing Conversion)指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l 和 d2f。注意,窄化类型转换很可能会造成精度丢失。
对象创建与操作指令
- 创建类实例的指令:
new - 创建数组的指令:
newarray、anewarray、multianewarray 访问类字段和实例字段的指令:
- 类字段(static 字段,或称为类变量):
getstatic、putstatic - 实例字段(非 static 字段,或称为实例变量):
getfield、putfield
- 类字段(static 字段,或称为类变量):
- 把一个数组元素加载到操作数栈的指令:
baload、caload、saload、iaload、laload、faload、daload、aaload - 将一个操作数栈的值储存到数组元素中的指令:
bastore、castore、sastore、iastore、fastore、dastore、aastore - 取数组长度的指令:
arraylength - 检查类实例类型的指令:
instanceof、checkcast
操作数栈管理指令
Java 虚拟机提供了一些用于直接操作操作数栈的指令,包括:pop、pop2、dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2 和 swap。
控制转移指令
- 条件分支:
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
方法调用和返回指令
invokevirtual:用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是 Java 语言中最常见的方法分派方式。invokeinterface:用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。invokespecial:用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。invokestatic:用于调用类方法(static 方法)。
方法返回指令则是根据返回值的类型区分的,包括 ireturn(当返回值是 boolean、byte、char、short 和 int 类型时使用)、lreturn、freturn、dreturn 和 areturn。另外还有一条 return 指令供声明为 void 的方法、实例初始化方法、类和接口的类初始化方法使用。
抛出异常指令
athrow
参考链接:关于 Java 虚拟机中的字节码指令
JVM 运行时数据区
源代码经过编译器编译之后便会生成一个字节码文件。字节码是一种二进制的类文件,它的内容是 JVM 的指令,而不像 C、C++ 经由编译器直接生成机器码。我们不用担心生成的字节码文件的兼容性,因为所有的 JVM 全部遵守 Java 虚拟机规范,也就是说所有的 JVM 环境都是一样的,这样一来字节码文件可以在各种 JVM 上运行(当然也包括 KVM)。
- 栈帧(Frame):每一个线程都有一个保存帧的栈。在每一个方法调用的时候创建一个帧。一个帧包括了三个部分:操作栈(Operand Stack)、局部变量数组(Local Variable Array),和一个对当前方法所属类的常量池的引用。
- 局部变量表:局部变量数组也被称之为局部变量表,它包含了方法的参数,也用于保存一些局部变量的值。参数值的存放总是在局部变量数组的 index 0 开始的。如果当前帧是由构造函数或者实例方法创建的,那么该对象引用(
this)将会存放在 location 0 处,然后才开始存放其余的参数。 - 操作栈:操作栈是一个(LIFO)栈,用于压入和取出值,其大小也在编译时决定。某些 Opcode 指令将值压入操作栈,其余的 Opcode 指令将操作数取出栈,使用它们后再把结果压入栈。操作栈也用于接收从方法中返回的值。
局部变量表的大小由编译时决定,同时也依赖于局部变量的数量和一些方法的大小。
实例分析:HelloWorld
以 HelloWorld 程序为例,经过命令:
E:\JavaExe>javap -c HelloWorld > HelloWorld.bytecode就会在目录下生成一个字节码文件,用编辑器打开后内容如下:
Compiled from "HelloWorld.java"
class HelloWorld extends java.lang.Object{
public HelloWorld(java.lang.String,int);
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2; //String
7: putfield #3; //Field name:Ljava/lang/String;
10: aload_0
11: iconst_0
12: putfield #4; //Field idNumber:I
15: aload_0
16: aload_1
17: putfield #3; //Field name:Ljava/lang/String;
20: aload_0
21: iload_2
22: putfield #4; //Field idNumber:I
25: aload_0
26: aload_1
27: iload_2
28: invokevirtual #5; //Method StoreData:(Ljava/lang/String;I)V
31: return
public void StoreData(java.lang.String,int);
Code:
0: bipush 90
2: istore_2
3: return
void print(AnotherClass);
Code:
0: aload_1
1: bipush 10
3: putfield #6; //Field AnotherClass.a:I
6: new #7; //class AnotherClass
9: dup
10: invokespecial #8; //Method AnotherClass."<init>":()V
13: astore_1
14: aload_1
15: bipush 20
17: putfield #6; //Field AnotherClass.a:I
20: return
}对应的 Java 源代码如下,我们可以对照源文件来查看一些重要的指令:
class HelloWorld {
private String name = "";
private int idNumber = 0;
public HelloWorld(String strName, int num) {
name = strName;
idNumber = num;
StoreData(strName, num);
}
public void StoreData(String str, int i) {
i = 90;
}
void print(AnotherClass another) {
another.a = 10;
another = new AnotherClass();
another.a = 20;
}
}
class AnotherClass {
public int a = 0;
}字节码指令详解
针对 void print(AnotherClass) 方法的字节码分析:
0: aload_1 // 把存放在局部变量表中索引 1 位置的对象引用压入操作栈
1: bipush 10 // 把整数 10 压入栈
3: putfield #6 // 把成员变量 a 的值设置成栈中的 10,#6 代表 6 号常量项
6: new #7 // 创建 AnotherClass 的对象,把引用放入栈
9: dup // 复制刚放入的引用(这时存在着两个相同的引用)
10: invokespecial #8 // 通过其中的一个引用调用 AnotherClass 的构造器,初始化对象
13: astore_1 // 把引用保存到局部变量表中的索引 1 位置中,然后引用弹出栈
14: aload_1 // 把局部变量表中索引 1 处的值压入操作栈
15: bipush 20 // 把整数 20 压入栈
17: putfield #6 // 把成员变量 a 的值设置成栈中的 20
20: return // 执行完毕退出针对构造函数 public HelloWorld(java.lang.String,int) 中的一段代码分析:
0: aload_0
// 将该(this)对象压入操作栈,对于实例方法和构造函数的局部变量表来说第一个入口总是这个"this"。
// 因为你需要访问一些实例中的方法和变量。
1: invokespecial #1; //Method java/lang/Object."<init>":()V
// 调用该类的超类构造函数,因为所有类都继承于 java.lang.Object。
// 而该类 (HelloWorld) 没有显式调用父类构造器,所以编译器提供必要的字节码来调用这些基类构造器。
4: aload_0
// 将该(this)对象压入操作栈
5: ldc #2; //String
// 加载字符串常量
7: putfield #3; //Field name:Ljava/lang/String;
// 把栈中的 name 的值置为栈中的"abc"(对应源码中的初始化或赋值)
10: aload_0
// 同样,将 this 压入栈
11: iconst_0
// 将 0 压入栈
12: putfield #4; //Field idNumber:I
// 将 idNumber 置为栈中的 0,就是上一句指令中的操作
15: aload_0
// 将 this 压入栈,this 总是位于局部变量表的 index 0 处!
16: aload_1
// 将位于局部变量表中位置 1 处的方法的形参 strName 压入栈
17: putfield #3; //Field name:Ljava/lang/String;
// 将 name 的值置为栈中的 strName
20: aload_0
// 将 this 压入栈
21: iload_2
// 将位于局部变量表中位置 2 处的方法形参 num 压入栈
22: putfield #4; //Field idNumber:I
// 同 17 号操作,赋值
25: aload_0
// 将 this 压入操作栈
26: aload_1
// 将 strName 压入栈
27: iload_2
// 将 num 压入栈
28: invokevirtual #5; //Method StoreData:(Ljava/lang/String;I)V
// 调用方法 StoreData
31: return
// 如果有 ()V 标志方法没有返回值字节码索引说明
我们观察发现,在每一个 Opcode 指令左边的位置序号都不是连续的:0, 1, 4, 5, 7, 10…… 为什么?
每一个方法都有一个对应的 ByteCode 序列,这些值对应着每一个 Opcode 和其参数存放的序列中的某一个索引值。为什么这些索引不是顺序的?既然每一个指令占据一个字节,那索引为什么不是 0, 1, 2 呢?
原因是:一些指令的参数占据了一些 Bytecode 数组空间。
比如:aload_0 指令没有参数,所以占有一个字节;第二个指令 invokespecial,由于它本身带有参数,结果它本身和参数分别就占据了一个位置,所以上面的 1 过了就不是 4(中间跳过了参数占用的字节索引)。
以下表格展示了指令与占用空间的示意关系:
| 指令 | 指令 | 字节 1 | 字节 2 | 后续指令 |
|---|---|---|---|---|
| aload_0 | invokespecial | 00 | 05 | return |
说明:本文涉及的部分指令(如jsr、ret等)在较新版本的 Java(Java 7 及以后)中已被废弃或移除,实际开发请以当前 JDK 版本规范为准。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
本文地址:https://1diff.fun/archives/java-zi-jie-ma-zhi-ling-ji.html
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。