基于 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 → Ca fe Babe。
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 几个最关键的结构
c
// 类/接口引用
CONSTANT_Class_info {
u1 tag; // 7
u2 name_index; // 指向 CONSTANT_Utf8(内部名如 java/lang/Object)
}
// 字段/方法引用
CONSTANT_Fieldref_info {
u1 tag; // 9 / 10 / 11
u2 class_index; // 指向 CONSTANT_Class
u2 name_and_type_index; // 指向 CONSTANT_NameAndType
}
// 名称 + 描述符对
CONSTANT_NameAndType_info {
u1 tag; // 12
u2 name_index; // Utf8:字段名或方法名
u2 descriptor_index; // Utf8:字段/方法描述符
}
// 字符串字面量
CONSTANT_String_info {
u1 tag; // 8
u2 string_index; // 指向 CONSTANT_Utf8
}
// 整数 / 浮点
CONSTANT_Integer_info { u1 tag; u4 bytes; } // 3
CONSTANT_Float_info { u1 tag; u4 bytes; } // 4(IEEE 754)
// 长整 / 双精(占两个槽位!)
CONSTANT_Long_info { u1 tag; u4 high_bytes; u4 low_bytes; } // 5
CONSTANT_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. 类、父类、接口索引
c
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 仅包含本类型直接声明的字段(不含继承的)。结构:
c
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)
c
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 内部多处挂载属性。通用格式:
c
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.1 Code 属性(最值得懂)
方法体就藏在这里。结构如下:
c
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.2 ConstantValue 属性(字段常量)
c
ConstantValue_attribute {
u2 attribute_name_index;
u4 attribute_length; // 恒为 2
u2 constantvalue_index; // 指向常量池里的常量
}
只有同时是 static final 的基本类型或 String 字段才用它,例如:
java
public static final int MAX = 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
理论太抽象?直接拆一个真实文件。先准备源码:
java
// Demo.java
public class Demo {
public static final int MAX = 100;
private int count;
public int inc() {
return count + 1;
}
}
编译并反汇编(详情见 jvm-compilation-tutorial.md 第 3 章):
bash
javac Demo.java
javap -v Demo.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辅助调试。