JVM规范第 4 章:class 文件格式

基于 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 BECa 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

反直觉点LongDouble 在常量池里占连续两项indexindex+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_MODULEthis_class 必须指向 module-infosuper_classinterfaces_countfields_countmethods_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;

编译后 MAXfield_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 辅助调试。
相关推荐
橘子汽水1681 小时前
Leetcode 236,437:二叉树的最近公共祖先,路径总和III
java·数据结构
数据狐(Datafox)1 小时前
1688商品详情API技术解析与落地应用(含标准 JSON 示例)
java·数据库·json
君顾11 小时前
AI智能商城实战指南:从架构设计到部署落地的完整技术方案
java·开发语言·多商户
Rain的Java大神之路1 小时前
SkyWalking从0-1部署成功实战
java·后端·架构
就叫飞六吧1 小时前
Spring 动态注册与移除 Bean 科普
java·后端·spring
IMPYLH1 小时前
HTML 的 <legend> 元素
java·前端·html
运行时异常1 小时前
WMS 仓储系统集成 AI Agent 实战】第 1 讲:从零搭建开发环境——pgvector 装不上、模型选错、依赖冲突,一天踩完三个大坑
java
夕除1 小时前
redis--008
java·jvm·数据库
wear工程师1 小时前
线程池队列满了会怎样?从拒绝策略看懂 ThreadPoolExecutor 的扩容顺序
java