Java 字节码进阶1:深入解析 class 文件结构
大家好,我是此林。
最近阅读了《深入理解 JVM 字节码》这本书,体会颇深,不由感叹学海无涯,JVM 字节码果然博大精深。接下来我会以通俗简练的语言,记录我的总结体会。
深入理解 Java 的 class 文件是学习字节码的第一步,今天我们就先来剖析 class 文件的内部结构。
1. 初识 class 文件
计算机科学领域有一句名言:计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决。
仔细想想,确实是这个道理。
JVM 就是个活生生的例子。Java 在设计之初就提出一个口号:一次编写,到处运行。
JVM 就是这样一个中间层,JVM 会把 Java 代码先编译成 平台无关性 的字节码,在真正运行的时候,JVM 会把字节码翻译成对应平台的 二进制机器码,从而实现了跨平台。
需要注意的是,Java 是平台无关性的语言,但 JVM 不是跨平台的,比如 Windows 电脑需要安装 Windows 版本的 JDK,Ubuntu 需要安装 Linux 版本的 JDK。
JDK 中自带的 javac 命令可以将 Java 源文件编译成 .class 文件。
class 文件由下面十个部分组成:
1. 魔数
2. 版本号
3. 常量池
4. 类访问标记
5. 类索引
6. 超类索引
7. 接口表索引
8. 字段表
9. 方法表
10. 属性表
2. 魔数(Magic Number)
人们经常通过文件后缀名来识别文件类型,比如看到 .png 后缀的文件,我们就知道这是个 png 图片文件。但仅仅通过文件名后缀来区分文件类型就很不靠谱,因为后缀名可以被随意修改。
所以魔数就诞生了。
大多数文件都以 固定的几个字节开头作为魔数,用来标识当前文件类型。
比如 PDF 文件的魔数是 %PDF(十六进制 0x255044462D),png 文件的魔数是 \x89PNG(十六进制 0x89504E47)。不过这些魔数的选择是由文件格式制定者自由选择的。
Java class 文件的魔数就很有意思,在16进制下 .class 文件的魔数为:CAFEBABE(咖啡宝宝),这个和 Java 的诞生就有千丝万缕的联系了。
JVM 虚拟机在加载 class 文件之前会先去检查前四个字节,也就是魔数,如果不是 0xCAFEBABE,那么就会抛出 java.lang.ClassFormatError 的异常。
3. 版本号
魔数之后的四个字节分别表示 副版本号 和 主版本号,Java 8 的 版本是 0x 00 00 00 34,转换为十进制也就是 52。JVM 虚拟机在解析这个类的时候就知道这是一个 Java 8 编译的类,如果 class 文件的主版本号高于 JVM 自身的版本号,加载该类会抛出 java.lang.UnSupportedClassVersionError 异常。
下表是 Java 版本号 和 Major version 的关系。
| Java 版本 | 主版本号 |
| Java 5 | 49 |
| Java 6 | 50 |
| Java 7 | 51 |
| Java 8 | 52 |
| ....... | ...... |
| Java 17 | 61 |
如下图,这个 class 文件是 jdk 17 编译的。

4. 常量池
紧随版本号之后的常量池数据区,常量池是 class 文件最复杂的数据结构。
对于 JVM 字节码来说,如果操作数是很常用的数字,比如 0,这些操作数就 直接内嵌到字节码了。如果是 字符串常量 或 较大的整数,class 文件 会把这些操作数存储到 常量池。
那么当使用到这些操作数时,会根据常量池的索引位置来查找。
常量池里每个常量项(cp_info)的第一个字节表示类型(tag),接下来的几个字节表示常量项的具体内容。
关于类型(tag),一共有 14 种,都以 CONSTANT 开头,info 结尾,如下表:
| Tag 值 | 常量项结构名 | 含义 |
|---|---|---|
| 1 | CONSTANT_Utf8_info | UTF-8 编码的字符串 |
| 3 | CONSTANT_Integer_info | 整型常量 |
| 4 | CONSTANT_Float_info | 浮点常量 |
| 5 | CONSTANT_Long_info | 长整型常量 |
| 6 | CONSTANT_Double_info | 双精度浮点常量 |
| 7 | CONSTANT_Class_info | 类或接口的符号引用 |
| 8 | CONSTANT_String_info | 字符串字面量引用 |
| 9 | CONSTANT_Fieldref_info | 字段符号引用 |
| 10 | CONSTANT_Methodref_info | 普通方法符号引用 |
| 11 | CONSTANT_InterfaceMethodref_info | 接口方法符号引用 |
| 12 | CONSTANT_NameAndType_info | 名称和描述符 |
| 15 | CONSTANT_MethodHandle_info | 方法句柄(Java 7 引入) |
| 16 | CONSTANT_MethodType_info | 方法类型(Java 7 引入) |
| 18 | CONSTANT_InvokeDynamic_info | 动态调用点(Java 7 引入) |
如果想看类文件的常量池,可以使用命令例如:javap -v 类名

也可以直接使用 jclasslib 插件。

需要注意的是,class 文件常量池 和 JVM 运行时常量池 是有区别的。
| 对比项 | Class 文件常量池 | JVM 运行时常量池 |
|---|---|---|
| 生命周期 | 编译期生成,存于 .class 文件 | 类加载后进入方法区,随类卸载而销毁 |
| 内容 | 符号引用 + 字面量 | 已解析的直接引用 + 动态常量 |
| 是否可变 | 不可变(写死在 class 文件) | 可变(运行时可新增,如 String.intern) |
| 功能 | 提供类加载所需的符号信息 | 提供运行期方法调用、字段访问等所需的实际引用 |
JVM 虚拟机在真正去加载 class 文件到 JVM 内存的时候,会把 符号引用 转为 内存直接引用。
5. 访问标记(Access flags)
紧随常量池之后的区域,用来标识一个类为 final、abstract 等。
类访问标记如下表:
| 标志名 | 值(十六进制) | 含义 |
|---|---|---|
ACC_PUBLIC | 0x0001 | 声明为 public,可被包外访问 |
ACC_FINAL | 0x0010 | 声明为 final,不可被继承 |
ACC_SUPER | 0x0020 | 使用 invokespecial 时需要特殊处理(几乎所有现代编译器都会设置) |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 声明为 abstract,不能实例化 |
ACC_SYNTHETIC | 0x1000 | 编译器自动生成,而不是用户源代码直接定义的 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解类 |
ACC_ENUM | 0x4000 | 标识这是一个枚举类 |
当前图中的这个 Hello 类,访问标记为 21(ACC_SUPER | ACC_PUBLIC)。

可以发现,这些访问标记是可以自由组合的,当然有一定规则,比如 ACC_PUBLIC 和 ACC_PRIVATE 就不能放在一起。
6. 类索引(this_class)、超类索引(super_name)、接口表索引(interfaces)
这三个就放在一起讲吧。
this_class 表示类索引。
super_class 表示直接父类的索引。
interfaces 表示该类实现的父接口。
它们实际上都是指向常量池的索引,表示类或接口的名字,在此不多赘述。
7. 字段表(fields)
紧随接口索引表之后的是字段表(fields),类中定义的字段会被存储到这个集合中,包括 static 和 非static 字段。
每个字段 field_info 的格式如下,这个 field_info 可以理解为定义字段的原数据。
field_info {
access_flags
name_index
descriptor_index
attributes_counts
attributes[attributes_counts]
}
access_flags: 字段的访问标记,如 pulibc、static、volatile 等。
name_index: 表示字段名,和之前说的类索引一样,也指向常量池的字符串常量。
descriptor_index: 字段描述符索引,指向常量池的字符串常量。
attributes_counts:属性个数
attributes[attributes_counts]:属性集合
这里的 descriptor_index 特别说明下,其实也就是表示某个字段的类型,一共有三种类型:
1. 原始类型:byte、int、char、double 等。
2. 引用类型:String、Integer、Double、自定义对象等,比如 String 类型的字段描述符为 “ Ljava/lang/String; ”,引用类型开头都会加 L,结尾加上分号作为分隔。
3. 数组类型:比如 int[] 的字段描述符为 “ [I ”,String[] 为 “ [Ljava/lang/String ”,都会加一个前置的 [ 。
8. 方法表
方法表和之前的字段表十分类似,类中定义的方法都会存储在这个集合里。
它也会有 method_info 用来描述方法。
method_info {
access_flags
name_index
descriptor_index
attributes_counts
attributes[attributes_counts]
}
对照字段表即可,在此不多赘述。
9. 属性表
属性表是 class 文件的最后一部分。
刚才方法表和字段表里就出现了属性。

不过属性不止可以在字段和方法里,class 顶层文件也可以有,不同的虚拟机厂商可以自定义属性,属性表的结构如下:
{
attributes_counts
attributes[attributes_counts]
}
这个 attributes[attributes_counts] 里包含了:
max_stack:操作数栈的最大深度
max_locals:局部变量表的大小
exception_table:代码内部异常表信息,如我们熟知 try-catch 语法就会生成对应的异常表。
今天我们详细分享了 class 字节码的内部的十大组成,相信你一定有所收获!
关注我吧,我是此林!
带你看不一样的世界!
更多推荐
所有评论(0)