JVM学习日记(十二)Day12

我们上一篇主要讲了字节码文件和方法的字节码指令的简单执行过程,相信大家对字节码又有了一个新的理解,今天咱们还是对字节码指令进行进一步的解析。

魔数

之前主包应该也有说过这个词但是应该没有详细的说(具体在哪里说主包已经忘了😁),之前也说过JVM不仅仅是可以运行Java的项目,其实也是可以运行其他语言的项目,因为JVM其实只认识字节码文件,只要编译后是字节码文件那么就可以在JVM上运行,那怎么确定他是不是字节码文件呢,这个就和魔数有关系,魔数是一个4字节固定的值,它的值是 ​**0xCAFEBABE​(16进制),即 ​ CA FE BA BE**,标志着他是一个合法的字节码文件,我们使用 xxd <文件名>可以查看,前4个字节就是魔数。(16进制一个字符可以用4位二进制表示,而一个字节8位也就是8bit,也就是说一个16进制字符等于0.5个字节)

我们一直说的字节码文件本质上其实就是一个二进制流,而JVM读取加载的其实也是这个二进制流,所以要让一个JVM运行程序不一定需要从磁盘去读取,也可以来自于网络只要符合字节码即魔数就可以被加载、识别、使用。

那这个流又是什么?我们常说的IO流这个流到底指的是什么?其实值的就是不分隔的以0和1组成的文件,为什么叫做流也就是一个抽象的概念,主包认为就算水一样从源头开始到尽头中间是没有隔断的,这个就是一个完整的流,而主包上面的图是一个16进制的那为什么又说是二进制流?因为这个是为了我们人阅读而特意解析的,其实在磁盘里任何文件都是二进制的流,只是用了不同的解析器解析给我们看的,不然一堆010101我们也看不懂的。

Class文件的结构

虽然昨天主包已经讲了字节码文件的各个部分,但是可能还是比较模糊,现在就根据昨天的内容总结一下具体的结构。大概就是这样画的丑凑合看。

这个是一个字节码文件的结构表格,但是随着JDK的版本不断变化,可能有所不同的大家做一个了解就可以了,类型这个U 表示使用后面的数字表示数量,合起来就是使用的字节数量,而后面几个info就是表了,因为表的数量是不确定的所以就没有像其他的一样表示了,字节码中所以的表都是2个部分组成,一个是计数器一个就是表,其实可以理解为索引了这个计数器。另外就是这个字符串常量池是从1开始计算的,所以数量是-1,那为什么是这样呢?因为有些情况不需要任何指向这个常量池,所以0就用来表示无任何指向。

字节码指令

字节码指令是指JVM使用一个字节长度的代表特定操作意义的数字(称为操作码),以及其后面跟着的0到多个代表此操作需要的参数(称为操作数)组成的。但由于JVM限制了长度所以操作码不能超过256条,

大部分的指令其实都包含了数据类型和操作,如iload,i表示int类型,load表示从局部变量表加载入操作数栈。

数据类型 大小 指令前缀 示例指令 说明
int 32 位 i iload, iadd, iconst_0 最常用,布尔/字符也用 int
long 64 位 l lload, ladd, lconst_0 需 2 个栈槽(占用 2 个位置)
float 32 位 f fload, fadd, fconst_0
double 64 位 d dload, dadd, dconst_0 需 2 个栈槽
byte 8 位 (无,用 i) bipush 实际按 int 处理
short 16 位 (无,用 i) sipush 实际按 int 处理
char 16 位 (无,用 i) i2c(转换指令) UTF-16 编码
boolean 1 位 (无,用 i) - JVM 中用 int 表示(0/1)
引用类型 不固定 a aload String,对象,数组都是引用类型

JVM 字节码指令按功能可分为 ​9 大类,涵盖数据操作、流程控制、对象操作等。以下是详细分类和代表性指令:

1. 常量加载指令(Load Constants)​

作用 ​:将常量(数字、字符串、null等)压入操作数栈。

指令 操作数 栈变化 作用 示例
iconst_0 无(隐含 0 [] → [0] int0 压入栈顶 int a = 0;
bipush 10 单字节立即数(10 [] → [10] 将单字节整数(-128~127)压入栈顶 int b = 10;
ldc #5 常量池索引(#5 [] → [value] 从常量池加载复杂常量(如字符串) String s = "hi";
aconst_null [] → [null] null 引用压入栈顶 Object o = null;

2. 局部变量操作指令(Local Variables)​

作用​:读取或存储局部变量表中的数据。

指令 操作数 栈变化 作用 示例
iload_1 无(隐含索引 1 [] → [value] 加载第 1 个 int 局部变量到栈顶 int x = localVar;
astore_2 无(隐含索引 2 [ref] → [] 将栈顶引用存储到第 2 个局部变量 o = new Object();
wide iload 256 2 字节索引(256 [] → [value] 加载第 256 个局部变量(扩展索引) 加载大索引变量

3. 算术运算指令(Arithmetic)​

作用​:对栈顶数据进行数学运算(加、减、乘、除等)。

指令 操作数 栈变化 作用 示例
iadd [a, b] → [a+b] int 加法(弹出两数,压入结果) int c = a + b;
fmul [f1, f2] → [f1*f2] float 乘法 float f = f1 * f2;
ineg [x] → [-x] int 取负 int y = -x;

4. 类型转换指令(Type Conversion)​

作用​:强制转换栈顶数据的类型。

指令 操作数 栈变化 作用 示例
i2f [int] → [float] int 转为 float (float) 10
d2i [double] → [int] double 转为 int(可能截断) (int) 3.14
checkcast 常量池索引 [ref] → [ref] 校验引用类型是否匹配指定类 (String) obj

5. 对象操作指令(Object Operations)​

作用​:创建对象、访问字段或数组元素。

指令 操作数 栈变化 作用 示例
new #3 类常量池索引(#3 [] → [ref] 创建类实例并压入引用 new Object()
getfield #5 字段常量池索引(#5 [ref] → [value] 读取对象的实例字段值 obj.field
aaload [array, index] → [element] 加载引用数组的指定元素 arr[0]

6. 方法调用指令(Method Invocation)​

作用​:调用静态方法、实例方法或动态方法。

指令 操作数 栈变化 作用 示例
invokevirtual #7 方法常量池索引(#7 [ref, arg1, arg2...] → [result] 调用实例方法(虚方法分派) obj.method()
invokestatic #9 方法常量池索引(#9 [arg1, arg2...] → [result] 调用静态方法 Math.max(a, b)
invokedynamic 动态调用点(BootstrapMethod) 按需操作栈 动态语言支持(如 Lambda) () -> {}

7. 流程控制指令(Control Flow)​

作用​:实现条件分支、循环、方法返回等控制逻辑。

指令 操作数 栈变化 作用 示例
ifeq +10 跳转偏移量(+10 [int] → [] 如果栈顶 int 为 0 则跳转 if (a == 0)
goto -20 跳转偏移量(-20 无条件跳转到指定偏移量 while (true)
ireturn [int] → [] 从方法返回 int return 42;

8. 栈操作指令(Stack Manipulation)​

作用​:调整操作数栈的结构(复制、丢弃、交换等)。

指令 操作数 栈变化 作用 示例
dup [a] → [a, a] 复制栈顶值 new Object()(双引用)
pop [a] → [] 丢弃栈顶值 丢弃返回值
swap [a, b] → [b, a] 交换栈顶两个值 交换变量值

9. 其他指令(Miscellaneous)​

作用​:同步、空操作等特殊功能。

指令 操作数 栈变化 作用 示例
monitorenter [ref] → [] 进入同步块(获取对象锁) synchronized (obj)
nop 空操作(占位或调试) 无实际作用

以上的字节码指令只是部分,具体的还是要上官网或者去搜索的,有兴趣的小伙伴可以自行去了解,这里推荐一位博主,写的非常好Java字节码指令详解,1 万字20 张图带你彻底掌握字节码指令 | 二哥的Java进阶之路

总结

本篇是对上一篇的补充,主要还是说的是字节码相关的内容,好了就这。