基于 Oracle 官方《The Java Virtual Machine Specification》第 4 章(Java SE 26)编写。
这是什么,为什么值得懂
读懂class文件格式,你就能:
- 看穿
javap -v反汇编输出的每一行究竟对应文件里的哪个字节; - 理解「为什么
long/double常量在常量池里占两项」这类反直觉规则; - 在排查诡异问题(如版本不兼容、桥方法、泛型擦除)时直达根源;
- 真正明白「JVM 与语言无关」是如何从文件层面落地的。
本章是规范性内容(与第 3 章的非规范性示例不同),它精确规定了每个字节的含义。
1. 文件的物理形态
class文件是一串8 位字节流,整体大端序(big-endian,高位在前)。规范定义了三个基础数据类型:
| 类型 | 含义 |
|---|---|
u1 | 1 字节无符号量 |
u2 | 2 字节无符号量 |
u4 | 4 字节无符号量 |
文件用类 C 的伪结构描述,结构里的成员称为item,连续存放、无填充对齐(不像 C 结构体那样有 padding)。
唯一一个"第 4 章之外"的规定:多字节数据一律大端序。这意味着不同硬件架构的机器读到的是完全一致的字节,这是「一次编译、到处运行」在文件层面的保证。
2. 整体结构:ClassFile
单个class文件只描述一个类、接口或模块,整体结构如下:
ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }注意几个要点:
magic:固定为0xCAFEBABE。这是 JVM 校验文件合法性的第一关——读到不是这个魔数的文件会直接拒绝(著名的「咖啡宝贝」梗)。constant_pool_count是项数 +1:有效的常量池索引范围是0 < index < constant_pool_count。第 0 项被保留(例如让「不指向任何常量」的字段能存 0)。- 所有
xxx_count后都紧跟一个xxx[]数组,长度即前面那个计数。
我会用「咖啡宝贝」帮你记住魔数:CA FE BA BE→CafeBabe。
3. 版本号:minor_version/major_version
- JVM 接受的主版本号范围:45 ~ 70(Java SE 26 支持到 70)。
- 当
major_version ≥ 56(即 Java 12+)时,minor_version必须为 0 或 65535:minor = 0:正式发布版;minor = 65535:预览(Preview)版本,把 class 锁死在单一发布版,不能跨版加载。
- 版本
70.65535表示「依赖 Java SE 26 的预览特性」。
实战提示:当你看到
UnsupportedClassVersionError: ... major version 65,意思是这份 class 用 JDK 21(major 65)编译,但你的运行环境低于它。对照表见jvm-intro-tutorial.md第 1 章。
4. 常量池(The Constant Pool)
常量池是class文件的「中央字典」,几乎所有其他部分都通过索引引用它。它是变长表的数组,灵活性最高。
4.1 通用格式
cp_info { u1 tag; u1 info[]; }tag决定后面info的结构。共17 种常量:
| 常量种类 | Tag | 说明 |
|---|---|---|
CONSTANT_Utf8 | 1 | 修改 UTF-8 字符串(名字、描述符等) |
CONSTANT_Integer | 3 | int常量 |
CONSTANT_Float | 4 | float常量 |
CONSTANT_Long | 5 | long常量(占两项) |
CONSTANT_Double | 6 | double常量(占两项) |
CONSTANT_Class | 7 | 类/接口的符号引用 |
CONSTANT_String | 8 | String字面量引用 |
CONSTANT_Fieldref | 9 | 字段的符号引用 |
CONSTANT_Methodref | 10 | 方法的符号引用 |
CONSTANT_InterfaceMethodref | 11 | 接口方法的符号引用 |
CONSTANT_NameAndType | 12 | 名称 + 描述符对 |
CONSTANT_MethodHandle | 15 | 方法句柄 |
CONSTANT_MethodType | 16 | 方法类型 |
CONSTANT_Dynamic | 17 | 动态常量(引导方法) |
CONSTANT_InvokeDynamic | 18 | invokedynamic引导 |
CONSTANT_Module | 19 | 模块名(仅模块文件) |
CONSTANT_Package | 20 | 包名(仅模块文件) |
4.2 几个最关键的结构
// 类/接口引用CONSTANT_Class_info{u1 tag;// 7u2 name_index;// 指向 CONSTANT_Utf8(内部名如 java/lang/Object)}// 字段/方法引用CONSTANT_Fieldref_info{u1 tag;// 9 / 10 / 11u2 class_index;// 指向 CONSTANT_Classu2 name_and_type_index;// 指向 CONSTANT_NameAndType}// 名称 + 描述符对CONSTANT_NameAndType_info{u1 tag;// 12u2 name_index;// Utf8:字段名或方法名u2 descriptor_index;// Utf8:字段/方法描述符}// 字符串字面量CONSTANT_String_info{u1 tag;// 8u2 string_index;// 指向 CONSTANT_Utf8}// 整数 / 浮点CONSTANT_Integer_info{u1 tag;u4 bytes;}// 3CONSTANT_Float_info{u1 tag;u4 bytes;}// 4(IEEE 754)// 长整 / 双精(占两个槽位!)CONSTANT_Long_info{u1 tag;u4 high_bytes;u4 low_bytes;}// 5CONSTANT_Double_info{u1 tag;u4 high_bytes;u4 low_bytes;}// 6反直觉点:Long和Double在常量池里占连续两项(index和index+1),且index+1不可用。原因是 64 位值无法塞进单个u4。
修改 UTF-8(Modified UTF-8):CONSTANT_Utf8用的是 JVM 自己定制的 UTF-8 变体——\u0000被编码成两字节的0xC0 0x80(保证字符串里绝不出现裸0x00,方便 C 风格处理),并只使用 1/2/3 字节形式。
5. 访问标志:access_flags
紧跟在常量池之后,this_class之前。它是一组位掩码,含义取决于当前是类、接口还是模块(表 4.1-B):
| 标志 | 值 | 含义 |
|---|---|---|
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 | 是enum类 |
ACC_MODULE | 0x8000 | 是模块(module-info)而非类/接口 |
若置了
ACC_MODULE:this_class必须指向module-info,super_class、interfaces_count、fields_count、methods_count全为 0,且必须含一个Module属性。
6. 类、父类、接口索引
u2 this_class;// 指向 CONSTANT_Class(本类)u2 super_class;// 0 表示没有父类(仅 java/lang/Object 如此);否则指向直接父类u2 interfaces_count;u2 interfaces[interfaces_count];// 每个元素指向 CONSTANT_Class(直接超接口)super_class:普通类要么指向java/lang/Object,要么为 0;接口必须指向Object。- 接口数组存的是「直接超接口」,不含间接继承的。
7. 字段表(Fields)
fields仅包含本类型直接声明的字段(不含继承的)。结构:
field_info{u2 access_flags;u2 name_index;// -> Utf8:字段名u2 descriptor_index;// -> Utf8:字段描述符(见第 9 节)u2 attributes_count;attribute_info attributes[attributes_count];}字段访问标志(表 4.5-A)与类标志是不同的一张表:
| 标志 | 值 | 含义 |
|---|---|---|
ACC_PUBLIC | 0x0001 | public |
ACC_PRIVATE | 0x0002 | private |
ACC_PROTECTED | 0x0004 | protected |
ACC_STATIC | 0x0008 | static |
ACC_FINAL | 0x0010 | final |
ACC_VOLATILE | 0x0040 | volatile |
ACC_TRANSIENT | 0x0080 | transient(序列化跳过) |
ACC_SYNTHETIC | 0x1000 | 编译器生成 |
ACC_ENUM | 0x4000 | enum 元素 |
接口里的字段必须是
ACC_PUBLIC | ACC_STATIC | ACC_FINAL(即public static final常量)。
8. 方法表(Methods)
method_info{u2 access_flags;u2 name_index;// -> Utf8:方法名u2 descriptor_index;// -> Utf8:方法描述符u2 attributes_count;attribute_info attributes[attributes_count];}方法访问标志(表 4.6-A):
| 标志 | 值 | 含义 |
|---|---|---|
ACC_PUBLIC | 0x0001 | public |
ACC_PRIVATE | 0x0002 | private |
ACC_PROTECTED | 0x0004 | protected |
ACC_STATIC | 0x0008 | static |
ACC_FINAL | 0x0010 | final |
ACC_SYNCHRONIZED | 0x0020 | synchronized |
ACC_BRIDGE | 0x0040 | 桥方法(泛型/协变返回生成) |
ACC_VARARGS | 0x0080 | 变长参数 |
ACC_NATIVE | 0x0100 | native |
ACC_ABSTRACT | 0x0400 | abstract |
ACC_STRICT | 0x0800 | strictfp(仅 major 46~60) |
ACC_SYNTHETIC | 0x1000 | 编译器生成 |
特殊方法名:
<init>:实例构造器;<clinit>:类/接口初始化器(static {}块),必须是static(major ≥ 51)。
方法真正的字节码并不在这里,而藏在它自己的
Code属性里(见第 10 节)。
9. 描述符(Descriptors)
描述符是用字符串编码「类型」的紧凑约定,避免重复存储完整类型名。
字段描述符
FieldType → BaseType | ClassType | ArrayType| BaseType | 对应类型 |
|---|---|
B | byte |
C | char |
D | double |
F | float |
I | int |
J | long |
S | short |
Z | boolean |
- 引用类型:
L ClassName ;(内部名,.换成/),例如Ljava/lang/Object; - 数组:
[前缀,例如[[[D表示double[][][](最多 255 维)
方法描述符
MethodDescriptor: ( {ParameterDescriptor} ) ReturnDescriptor ReturnDescriptor: FieldType | V // V 表示 void- 参数描述符依次列出,不含
this; - 参数总「字数」(word,long/double 计 2,其余计 1)≤ 255。
示例:
Object m(int i, double d, Thread t)→(IDLjava/lang/Thread;)Ljava/lang/Object;
10. 属性(Attributes)
属性是class文件的「可扩展插槽」——你能在类、字段、方法、Code 内部多处挂载属性。通用格式:
attribute_info{u2 attribute_name_index;// -> Utf8:属性名u4 attribute_length;u1 info[attribute_length];}第 4 章预定义了约 30 个属性(按编号摘选关键的):
| 属性 | 编号 | 挂载位置 | 作用 |
|---|---|---|---|
ConstantValue | 4.7.2 | 字段 | 常量字段的初始值(如static final int) |
Code | 4.7.3 | 方法 | 方法字节码、异常表、局部变量槽数等 |
StackMapTable | 4.7.4 | Code | 类型校验所需的栈映射 |
Exceptions | 4.7.5 | 方法 | 受检异常列表 |
InnerClasses | 4.7.6 | 类 | 内部类关系 |
EnclosingMethod | 4.7.7 | 类 | 局部/匿名类的外围方法 |
Synthetic | 4.7.8 | 类/字段/方法 | 标记编译器生成 |
Signature | 4.7.9 | 类/字段/方法 | 泛型签名(擦除后保留) |
SourceFile | 4.7.10 | 类 | 源文件名 |
LineNumberTable | 4.7.12 | Code | 字节码偏移 ↔ 源码行号 |
LocalVariableTable | 4.7.13 | Code | 局部变量名与槽位 |
Deprecated | 4.7.15 | 类/字段/方法 | 标记已废弃 |
10.1Code属性(最值得懂)
方法体就藏在这里。结构如下:
Code_attribute{u2 attribute_name_index;u4 attribute_length;u2 max_stack;// 操作数栈最大深度u2 max_locals;// 局部变量表所需槽数(long/double 占 2)u4 code_length;u1 code[code_length];// 真正的字节码指令u2 exception_table_length;{u2 start_pc;u2 end_pc;u2 handler_pc;u2 catch_type;}exception_table[exception_table_length];u2 attributes_count;attribute_info attributes[attributes_count];// 常含 LineNumberTable 等}max_stack/max_locals:JVM 加载时就知道要分配多少栈和局部变量空间;exception_table:就是第 3 章讲过的「try-catch靠异常表而非跳转指令」的文件级实现;<init>/<clinit>等方法的字节码都在此。
10.2ConstantValue属性(字段常量)
ConstantValue_attribute{u2 attribute_name_index;u4 attribute_length;// 恒为 2u2 constantvalue_index;// 指向常量池里的常量}只有同时是static final的基本类型或String字段才用它,例如:
publicstaticfinalintMAX=100;编译后MAX的field_info会带ACC_PUBLIC | ACC_STATIC | ACC_FINAL,并挂一个ConstantValue,直接指向常量池里CONSTANT_Integer(100)。
11. 名字的约定
- 二进制名(Binary Name):内部形式用
/代替.,例如java/lang/Thread,存于CONSTANT_Utf8。 - 非限定名(Unqualified Name):字段/方法名不含
. ; [ /,方法名除<init>/<clinit>外也不含< >。 - 模块/包名:存于
CONSTANT_Module_info/CONSTANT_Package_info(模块文件专用)。
12. 动手:用javap -v拆解真实 class
理论太抽象?直接拆一个真实文件。先准备源码:
// Demo.javapublicclassDemo{publicstaticfinalintMAX=100;privateintcount;publicintinc(){returncount+1;}}编译并反汇编(详情见jvm-compilation-tutorial.md第 3 章):
javac Demo.java javap-vDemo.class你会看到输出层层对应本章结构:
Classfile Demo.class Last modified ... minor version: 0 major version: 65 // 对照表:JDK 21 flags: (0x0021) ACC_PUBLIC, ACC_SUPER this_class: #2 // Demo super_class: #3 // java/lang/Object interfaces: 0 Constant pool: // 第 4 节常量池 #1 = Methodref #3.#4 // java/lang/Object."<init>":()V #2 = Class #5 // Demo #3 = Class #6 // java/lang/Object ... { public static final int MAX; descriptor: I flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL ConstantValue: int 100 // 第 10.2 节 public Demo(); descriptor: ()V flags: (0x0001) ACC_PUBLIC Code: // 第 10.1 节 stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public int inc(); ... }每一行都能在第 4 章找到出处——这就是「规范即地图」的感觉。
小结
- 整体:
magic+ 版本 + 常量池 + 类/父类/接口 + 字段 + 方法 + 属性; - 常量池是中央字典,17 种常量,64 位值占双槽;
- 描述符用紧凑字符串编码类型,是贯穿全章的「类型语言」;
- 属性是扩展机制,
Code存字节码、ConstantValue存常量、SourceFile/LineNumberTable辅助调试。