Class 文件结构与字节码指令集
Class 文件是一组以 8 字节为最小单位的二进制流,无分隔符,数据项严格按顺序紧凑排列。Java 虚拟机规范定义了它的完整格式,掌握它才能看懂字节码、理解 JIT 与反编译产物。
Class 文件整体结构
一个 Class 文件由以下数据项按顺序组成:
| 数据项 | 类型 | 数量 | 说明 |
|---|---|---|---|
| magic | u4 | 1 | 魔数,固定 0xCAFEBABE |
| minor_version | u2 | 1 | 次版本号 |
| major_version | u2 | 1 | 主版本号(JDK 8 = 52,JDK 11 = 55,JDK 17 = 61) |
| constant_pool_count | u2 | 1 | 常量池容量计数(从 1 开始) |
| constant_pool | cp_info | n | 常量池表 |
| access_flags | u2 | 1 | 访问标志(public/final/abstract 等) |
| this_class | u2 | 1 | 类索引,指向常量池 |
| super_class | u2 | 1 | 父类索引 |
| interfaces_count | u2 | 1 | 接口数量 |
| interfaces | u2 | n | 接口索引表 |
| fields_count | u2 | 1 | 字段数量 |
| fields | field_info | n | 字段表 |
| methods_count | u2 | 1 | 方法数量 |
| methods | method_info | n | 方法表 |
| attributes_count | u2 | 1 | 属性数量 |
| attributes | attribute_info | n | 属性表 |
魔数与版本号
- 魔数:
0xCAFEBABE,用于识别文件是否为 Class 文件 - 版本号:主版本号决定了 JVM 能否解析该文件。JDK 向后兼容,高版本 JDK 可运行低版本编译的类,反之报
UnsupportedClassVersionError
常量池
常量池是 Class 文件中的"资源仓库",几乎所有数据项都引用它。每种常量都有唯一的 tag 标识类型:
| tag | 常量类型 | 说明 |
|---|---|---|
| 1 | CONSTANT_Utf8 | UTF-8 编码字符串(类名、字段名、方法名的载体) |
| 3 | CONSTANT_Integer | int 常量 |
| 4 | CONSTANT_Float | float 常量 |
| 7 | CONSTANT_Class | 类或接口的符号引用 |
| 8 | CONSTANT_String | 字符串字面量的符号引用 |
| 9/10/11 | Fieldref / Methodref / InterfaceMethodref | 字段/方法/接口方法的符号引用 |
| 12 | CONSTANT_NameAndType | 字段或方法的部分符号引用(名称+描述符) |
一个典型的 CONSTANT_Methodref 引用链:
CONSTANT_Methodref
├─ class_index ──→ CONSTANT_Class ──→ CONSTANT_Utf8(类名)
└─ name_and_type_index ──→ CONSTANT_NameAndType
├─ name_index ──→ CONSTANT_Utf8(方法名)
└─ descriptor_index ──→ CONSTANT_Utf8(描述符)访问标志与字段/方法表
access_flags
| 标志 | 含义 |
|---|---|
| ACC_PUBLIC (0x0001) | public |
| ACC_FINAL (0x0010) | final |
| ACC_SUPER (0x0020) | 使用新版 invokespecial 语义(默认开启) |
| ACC_INTERFACE (0x0200) | 接口 |
| ACC_ABSTRACT (0x0400) | abstract |
field_info / method_info
字段与方法的结构相同,均由 access_flags + name_index + descriptor_index + attributes 组成。
字段描述符:
| 描述符 | 类型 |
|---|---|
| B / C / D / F / I / J / S / Z | byte / char / double / float / int / long / short / boolean |
L类名; | 对象类型 |
| [` + 元素类型 | 数组(如 [I 表示 int[]) |
方法描述符:参数列表 + 返回类型,如 (ID)I 表示 int f(int, double)。
常用字节码指令
字节码指令按功能分类,常见类型如下:
加载与存储指令
iload_1 将局部变量表下标 1 的 int 压入操作数栈
aload_0 将局部变量表下标 0 的引用(this)压栈
istore_2 弹出 int 存入局部变量表下标 2
bipush 100 将字节常量 100 压栈
ldc "abc" 将常量池中的字符串压栈运算指令
iadd / isub / imul / idiv / irem int 加减乘除取余
iinc 1, 5 局部变量表下标 1 自增 5(i++ 的底层)类型转换与对象
i2l / i2d int 转 long / double
new 创建对象,压入引用
newarray 创建基本类型数组
getfield / putfield 读写实例字段
getstatic / putstatic 读写静态字段
checkcast 类型强转检查
instanceof 类型判断方法调用指令
invokevirtual 调用实例方法(多态分派)
invokestatic 调用静态方法
invokespecial 调用构造器、private 方法、super 方法
invokeinterface 调用接口方法
invokedynamic 动态方法调用(Lambda 表达式的实现基础)控制流指令
goto 无条件跳转
ifeq / ifne 栈顶等于/不等于 0 时跳转
if_icmpge 比较两个 int,>= 时跳转
tableswitch 密集 case 的 switch 跳转表
lookupswitch 稀疏 case 的 switch用 javap 查看字节码
javap 是查看 Class 结构最常用的工具:
bash
javap -v Hello.class # 完整输出:常量池、字段、方法、字节码、属性
javap -p Hello.class # 含私有成员
javap -c Hello.class # 仅反汇编方法字节码
javap -s Hello.class # 输出签名(描述符)一段源码与字节码的对照:
java
public static int add(int a, int b) {
return a + b;
}text
public static int add(int, int);
descriptor: (II)I
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
0: iload_0
1: iload_1
2: iadd
3: ireturnjavap -v 输出的常量池、访问标志、Code 属性与 Class 文件规范中的字段一一对应,是学习字节码的入门利器。
常见问题
- UnsupportedClassVersionError:主版本号大于运行 JDK 版本,换高版本 JDK 或降编译目标版本
- VerifyError:字节码验证失败,多为修改字节码的框架(如热部署)生成非法指令
- NoSuchMethodError:编译期引用的方法签名在运行期不存在,常见于依赖版本不一致