前言:
通过了解字节码文件可以帮助我们更容易的理解JVM的工作原理,所以接下来,我们来介绍一下字节码文件。
目录
[1. 魔数(Magic Number)](#1. 魔数(Magic Number))
[2. 版本号(Version Information)](#2. 版本号(Version Information))
[3. 常量池(Constant Pool)](#3. 常量池(Constant Pool))
[4. 访问标志(Access Flags)](#4. 访问标志(Access Flags))
[5. 类/父类/接口信息](#5. 类/父类/接口信息)
[6. 字段表(Fields)](#6. 字段表(Fields))
[7. 方法表(Methods)](#7. 方法表(Methods))
[8. 属性表(Attributes)](#8. 属性表(Attributes))
正确的打开字节码文件
字节码文件想要正确打开需要使用工具,它保存的是源码编译之后的结果是二进制,如果直接打开我们是看不懂的,推荐jclasslib我们可以直接在idea中安装

通过View下划红线位置打开


打开之后我们就可以清晰的看见字节码文件的组成。现在我们来具体的介绍一下这些信息。
字节码文件组成
1. 魔数(Magic Number)
-
位置 :文件起始的4字节(
0xCAFEBABE
)。 -
作用 :标识这是一个合法的
.class
文件(JVM加载时会首先验证此值)。
在一般信息我们是看不见它的,但是在每个java字节码文件中都会有它,没有它就不能叫java字节码文件。
2. 版本号(Version Information)
-
组成:
-
次版本号(Minor Version) :2字节(通常为
0
)。 -
主版本号(Major Version) :2字节(例如jdk8为
52
,jdk11为55
)。
-
-
作用:JVM据此判断是否兼容该字节码文件。
我们可以通过主版本号-44得到jdk版本, (例如jdk8为52
,jdk11为55
)。
3. 常量池(Constant Pool)
-
核心地位 :字节码中占比最大的部分,存储所有字面量 和符号引用。
-
结构:
-
常量池计数器(Constant Pool Count):2字节,表示常量数量(实际数量 = 计数值 - 1)。
-
常量池表(Constant Pool Entries) :每个条目结构不同,类型由1字节的
tag
标识。
-
常量池可以避免我们的内容重复,节省空间。
4. 访问标志(Access Flags)
-
位置:常量池之后的2字节。
-
作用 :描述类/接口的访问属性(如
public
、final
、abstract
等)。 -
常见标志位:
-
ACC_PUBLIC
(0x0001) -
ACC_FINAL
(0x0010) -
ACC_INTERFACE
(0x0200) -
ACC_ENUM
(0x4000)
-
5. 类/父类/接口信息
-
当前类索引(This Class) :2字节,指向常量池中
CONSTANT_Class
条目。 -
父类索引(Super Class) :2字节(
0
表示继承java.lang.Object
)。 -
接口表(Interfaces):
-
接口计数器(2字节)
-
接口索引集合(每个索引2字节,指向常量池)。
-
6. 字段表(Fields)
-
组成:
-
字段计数器(2字节)
-
字段详细信息表(每个字段包含访问标志、名称索引、描述符索引等)。
-
-
描述符(Descriptor) :描述字段类型(如
I
表示int
,Ljava/lang/String;
表示字符串)。
7. 方法表(Methods)
-
结构:
-
方法计数器(2字节)
-
方法详细信息表(每个方法包含访问标志、名称索引、描述符索引等)。
-
-
关键属性 :每个方法内嵌一个
Code
属性(见下文),存储实际字节码指令。
8. 属性表(Attributes)
-
通用结构:
-
属性计数器(2字节)
-
属性信息集合(每个属性包含名称索引、长度、自定义数据)。
-
-
核心属性类型:
-
Code
属性:存储方法的字节码指令、操作数栈深度、局部变量表等。 -
LineNumberTable
:映射字节码偏移量到源代码行号(调试用)。 -
SourceFile
:源文件名(如HelloWorld.java
)。 -
Exceptions
:方法声明的受检异常。 -
Synthetic
:标记编译器生成的成员。
-
关键特点
-
紧凑性 :所有数据以无符号数(
u1
/u2
/u4
)紧凑存储。 -
符号引用:类/方法/字段名均以常量池索引形式存在。
-
可扩展性 :通过属性表支持自定义扩展(如注解信息存储在
RuntimeVisibleAnnotations
属性中)。
阅读字节码文件
0 iconst_0 // 将int类型常量0压入操作数栈顶
1 istore_1 // 将操作数栈顶的int类型数值(0)存入第二个局部变量槽中(局部变量索引1)
2 iload_1 // 从局部变量表中加载索引为1的int类型值到操作数栈顶
3 iinc 1 by 1 // 将局部变量表中索引为1的int类型变量增加1
6 istore_1 // 将操作数栈顶的int类型数值(经过iinc后的值)存入第二个局部变量槽中(局部变量索引1)
7 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;> // 获取类java.lang.System的静态字段out的值,即PrintStream对象,并压入操作数栈顶
10 iload_1 // 从局部变量表中加载索引为1的int类型值到操作数栈顶
11 invokevirtual #3 <java/io/PrintStream.println : (I)V> // 调用PrintStream对象的println方法打印int值
14 return // 从当前方法返回
我们来看看它的源代码。

所以这个时候i的值就是0,可以跟着注释一步步走你就能清楚了。
0 iconst_0
1 istore_1
2 iinc 1 by 1
5 iload_1
6 istore_1
7 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
10 iload_1
11 invokevirtual #3 <java/io/PrintStream.println : (I)V>
14 return
它的源码

可以看见两个代码的字节码指令的iinc 1 by 1和iload_1的位置不同,也就是我们常说的,i++先赋值在自增,而++i是先自增在赋值。所以两个代码的值分别是0和1
总结
希望通过这篇文章,可以让你对字节码文件的认识更加清晰,通过学习字节码文件我们也算是接触到了更加深层次的代码学习,可以让我们从最底层的逻辑来学习代码的执行。感谢你的阅读,你的阅读和点赞是我最大动力。
