JVM类文件结构详解
约 1751 字大约 6 分钟
javajvmclass-file
2025-03-08
概述
Java源文件经过编译后生成 .class 文件,这是一种平台无关的二进制格式,由JVM规范严格定义。理解Class文件结构是深入JVM的第一步,也是理解字节码、类加载、动态代理等机制的基础。
Class文件是一组以8位字节为基础单位的二进制流,各数据项严格按顺序紧凑排列,没有任何分隔符。
整体结构
完整结构定义:
ClassFile {
u4 magic; // 魔数
u2 minor_version; // 次版本号
u2 major_version; // 主版本号
u2 constant_pool_count; // 常量池计数
cp_info constant_pool[count-1]; // 常量池
u2 access_flags; // 访问标志
u2 this_class; // 类索引
u2 super_class; // 父类索引
u2 interfaces_count; // 接口计数
u2 interfaces[count]; // 接口索引表
u2 fields_count; // 字段计数
field_info fields[count]; // 字段表
u2 methods_count; // 方法计数
method_info methods[count]; // 方法表
u2 attributes_count; // 属性计数
attribute_info attributes[count]; // 属性表
}魔数与版本号
CA FE BA BE 00 00 00 3D
|__________| |__| |__|
魔数 次版本 主版本(61=Java 17)| 主版本号 | 对应JDK |
|---|---|
| 52 (0x34) | Java 8 |
| 55 (0x37) | Java 11 |
| 61 (0x3D) | Java 17 |
| 65 (0x41) | Java 21 |
JVM 向下兼容:高版本JVM可以执行低版本Class文件,反之不行。
常量池(Constant Pool)
常量池是Class文件中最大、最复杂的部分,存储了类中使用的所有字面量和符号引用。索引从 1 开始(0保留表示"不引用")。
常量池类型
| 标志值(tag) | 类型 | 描述 |
|---|---|---|
| 1 | CONSTANT_Utf8 | UTF-8编码的字符串 |
| 3 | CONSTANT_Integer | int字面量 |
| 4 | CONSTANT_Float | float字面量 |
| 5 | CONSTANT_Long | long字面量(占两个槽位) |
| 6 | CONSTANT_Double | double字面量(占两个槽位) |
| 7 | CONSTANT_Class | 类或接口的符号引用 |
| 8 | CONSTANT_String | String字面量 |
| 9 | CONSTANT_Fieldref | 字段的符号引用 |
| 10 | CONSTANT_Methodref | 方法的符号引用 |
| 11 | CONSTANT_InterfaceMethodref | 接口方法的符号引用 |
| 12 | CONSTANT_NameAndType | 名称和描述符 |
| 15 | CONSTANT_MethodHandle | 方法句柄(Java 7+) |
| 16 | CONSTANT_MethodType | 方法类型(Java 7+) |
| 18 | CONSTANT_InvokeDynamic | 动态调用点(Java 7+) |
符号引用关系
访问标志(access_flags)
// 类/接口的访问标志
ACC_PUBLIC 0x0001 // public
ACC_FINAL 0x0010 // final
ACC_SUPER 0x0020 // 使用invokespecial新语义(JDK 1.0.2后默认设置)
ACC_INTERFACE 0x0200 // 接口
ACC_ABSTRACT 0x0400 // 抽象类
ACC_SYNTHETIC 0x1000 // 编译器生成
ACC_ANNOTATION 0x2000 // 注解类型
ACC_ENUM 0x4000 // 枚举类型
ACC_MODULE 0x8000 // 模块(Java 9+)例如 public final class Foo 的 access_flags = 0x0001 | 0x0010 | 0x0020 = 0x0031。
字段表(Fields)
field_info {
u2 access_flags; // 字段访问标志
u2 name_index; // 字段名(常量池UTF8索引)
u2 descriptor_index; // 描述符(常量池UTF8索引)
u2 attributes_count;
attribute_info attributes[]; // 如ConstantValue属性
}字段描述符
| 描述符 | 类型 |
|---|---|
| B | byte |
| C | char |
| D | double |
| F | float |
| I | int |
| J | long |
| S | short |
| Z | boolean |
| LClassName; | 对象类型 |
| [ | 数组维度 |
示例:
int count→IString name→Ljava/lang/String;int[][] matrix→[[IObject[] arr→[Ljava/lang/Object;
方法表(Methods)
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[]; // 最重要的是Code属性
}方法描述符
格式:(参数类型列表)返回类型
void main(String[] args) → ([Ljava/lang/String;)V
int add(int a, int b) → (II)I
String toString() → ()Ljava/lang/String;
void <init>() → ()V (构造方法)
void <clinit>() → ()V (类初始化方法)属性表(Attributes)
属性表是Class文件中最灵活的部分,可以出现在Class、Field、Method、Code等多个层级。
Code属性(最核心)
Code_attribute {
u2 attribute_name_index; // "Code"
u4 attribute_length;
u2 max_stack; // 操作数栈最大深度
u2 max_locals; // 局部变量表大小
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[count];
u2 attributes_count; // Code内部的属性
attribute_info attributes[]; // 如LineNumberTable、LocalVariableTable
}其他重要属性
| 属性 | 位置 | 作用 |
|---|---|---|
| ConstantValue | field | final static字段的编译期常量值 |
| Exceptions | method | 方法throws声明的异常 |
| LineNumberTable | Code | 字节码行号↔源码行号映射 |
| LocalVariableTable | Code | 局部变量名和描述符 |
| SourceFile | class | 源文件名 |
| InnerClasses | class | 内部类信息 |
| StackMapTable | Code | 类型检查验证(Java 6+) |
| BootstrapMethods | class | invokedynamic引导方法表 |
| RuntimeVisibleAnnotations | class/field/method | 运行时可见注解 |
| Record | class | Record组件信息(Java 16+) |
| PermittedSubclasses | class | Sealed类允许的子类(Java 17+) |
javap分析实例
// 源代码
public class Hello {
private int count;
public int increment() {
return ++count;
}
public static void main(String[] args) {
Hello h = new Hello();
System.out.println(h.increment());
}
}使用 javap -v -p Hello.class 反编译:
Classfile /path/to/Hello.class
Last modified ...; size 505 bytes
SHA-256 checksum ...
Compiled from "Hello.java"
public class Hello
minor version: 0
major version: 61
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #8 // Hello
super_class: #2 // java/lang/Object
interfaces: 0, fields: 1, methods: 3, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // Hello.count:I
#8 = Class #10 // Hello
...
{
private int count;
descriptor: I
flags: (0x0002) ACC_PRIVATE
public Hello();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int increment();
descriptor: ()I
flags: (0x0001) ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0 // 加载this
1: aload_0
2: getfield #7 // 获取this.count
5: iconst_1 // 压入常量1
6: iadd // 相加
7: dup_x1 // 复制结果
8: putfield #7 // 存回this.count
11: ireturn // 返回结果
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #8 // class Hello
3: dup
4: invokespecial #1 // Method "<init>":()V
7: astore_1
8: getstatic #12 // Field System.out
11: aload_1
12: invokevirtual #14 // Method increment:()I
15: invokevirtual #16 // Method PrintStream.println:(I)V
18: return
}类文件验证流程
常见FAQ
Q: Class文件一定来自Java源码吗?
A: 不一定。Kotlin、Scala、Groovy、Clojure 等JVM语言都编译为Class文件。只要符合JVM规范,JVM不关心源语言。
Q: 为什么常量池索引从1开始?
A: 0号索引有特殊含义 — 表示"不引用任何常量"。例如 super_class = 0 表示没有父类(只有 java.lang.Object 如此)。
Q: 一个方法最大能有多少字节码?
A: Code属性中 code_length 是 u4(32位),但JVM规范限制方法字节码不超过 65535 字节(u2 范围)。
总结
Class文件是Java跨平台的基石。它用紧凑的二进制格式编码了类的所有元信息:常量池存储符号引用和字面量,方法表中的Code属性存储字节码指令,属性表提供调试信息和扩展信息。掌握Class文件结构,是理解类加载、字节码增强(如AOP、动态代理)和JVM调优的前提。
贡献者
更新日志
9f6c2-feat: organize wiki content and refresh site setup于