Skip to content

01.1 - Class 文件结构

定位: JVM 的"可执行文件格式",Java 跨平台的 OS 隔离层 面试高频度: ⭐⭐⭐⭐⭐ 考查方式: 常量池条目分类、属性表的作用、魔数含义、字段描述符编码

一、这是什么?为什么需要它?

C 语言编译成 .exe.out——格式是平台相关的(Windows PE、Linux ELF)。Java 想要 "一次编译,到处运行",就不能直接生成机器码,而是需要一种与操作系统无关的中间表示

这就是 Class 文件。javac 将所有平台差异消解在字节码层面,任何平台的 JVM 都能加载同一个 Class 文件。

C 世界:  .c → 编译器 → .exe (Windows) / .elf (Linux) / .macho (macOS)
Java 世界: .java → javac → .class (所有平台一致)

                     Windows JVM / Linux JVM / macOS JVM 各自加载

二、原理拆解

Class 文件是一个严格定义的大端序二进制结构。每个字段的排列顺序、大小、含义都由规范严格规定。

2.1 魔数 (Magic Number) 0xCAFEBABE

为什么存在: 文件类型标识。JVM 加载 Class 文件时,先读取前 4 字节,如果不是 0xCAFEBABE,直接拒绝加载——第一道安全门

类比:PDF 文件的 %PDF、PNG 的 ‰PNG。都是让加载器快速判断"这个文件是我能处理的类型吗?"

故事:James Gosling 在 Oak 项目早期和同事去一家名为"Cafe Dead"的咖啡馆,由此得名 0xCAFEBABE。后来 Java 的商标设计中也有咖啡元素。

2.2 版本号 (Minor / Major Version)

小版本号 + 主版本号,共 4 字节。例如 JDK 8 = major 52 (0x34), minor 0。

为什么存在: 向前兼容控制。低版本 JVM 拒绝高版本 Class 文件(高版本可能用了低版本不认识的指令或结构)。例如 JDK 8 JVM 不能加载 JDK 11 编译的 Class 文件(major 55)。

JDK 1.1 → major 45    JDK 8 → major 52
JDK 11  → major 55    JDK 17 → major 61
JDK 21  → major 65

类比:Word 2019 打不开 Word 2021 保存的文档格式更新。

2.3 常量池 (Constant Pool)

常量池是 Class 文件的资源仓库,也是最复杂的区域。它记录了类中用到的几乎所有常量信息:类名、方法名、字段名、字符串字面量、数值常量等。

为什么存在: 字节码指令本身很简短(单字节操作码),没法直接携带完整的类名和方法签名。指令通过索引号指向常量池条目,常量池条目中保存着完整的字符串信息。这种间接引用是动态链接的基础。

为什么需要两级常量池:

┌─ 编译时 ──────────────────────┐      ┌─ 运行时 ─────────────────────┐
│  Class 文件常量池 (静态模板)    │      │  运行时常量池 (动态实例)      │
│                                │      │                              │
│  #1 = Class "java/lang/Object" │ 加载  │  #1 → Class 对象引用 (已解析) │
│  #2 = Methodref #1.#3         │ ───→  │  #2 → 方法入口地址 (已解析)   │
│  #3 = Utf8 "<init>"           │      │  #3 = Utf8 "<init>"          │
│                                │      │  #N = String.intern() 动态加入│
└────────────────────────────────┘      └──────────────────────────────┘
  • 静态池存的是符号引用(字符串形式的类名、方法描述符),编译时生成
  • 运行时池在类加载时被创建,将符号引用逐步解析为直接引用(内存地址),并且可以通过 String.intern() 动态添加新常量

这种"间接引用 → 直接引用"的分步设计,使得多态成为可能——编译时不知道实际调用哪个方法,运行时才解析。

2.4 访问标志 (Access Flags)

2 字节,标记该 Class 是 public、final、abstract、interface、enum、annotation 等。

ACC_PUBLIC    0x0001   // 公开类
ACC_FINAL     0x0010   // 不可继承 (final class)
ACC_SUPER     0x0020   // 始终为 true (JDK 1.2+)
ACC_INTERFACE 0x0200   // 接口
ACC_ABSTRACT  0x0400   // 抽象类/接口
ACC_ANNOTATION 0x2000  // 注解
ACC_ENUM      0x4000   // 枚举

ACC_SUPER 在 JDK 1.2 之后始终为 true,用于标识该类使用增强的 invokespecial 语义。

2.5 类索引 / 父类索引 / 接口索引

三个 u2 类型,指向常量池中的 CONSTANT_Class_info 条目:

  • this_class: 当前类的索引
  • super_class: 父类的索引(只有 java.lang.Object 为 0)
  • interfaces[]: 接口索引数组

为什么用索引而不是直接存类名: 复用常量池中的字符串。类名已经在常量池的 CONSTANT_Utf8_info 中存了一份,索引引用即可,节省空间。

2.6 字段表 (Fields)

描述类/实例中声明的每个字段。每个字段的结构:

field_info {
    u2             access_flags;     // ACC_PRIVATE, ACC_STATIC, ACC_VOLATILE...
    u2             name_index;       // 指向常量池中字段名的 Utf8 条目
    u2             descriptor_index; // 指向常量池中描述符的 Utf8 条目
    u2             attributes_count; // 属性表数量
    attribute_info attributes[];     // 属性表(如 ConstantValue)
}

字段描述符编码规则——JVM 用紧凑的字符串编码类型信息:

描述符含义
Iint
Jlong
Ddouble
Ffloat
Zboolean
Bbyte
Cchar
Sshort
Vvoid
L类名;引用类型,如 Ljava/lang/String;
[Iint[] 一维数组
[[Jlong[][] 二维数组

2.7 方法表 (Methods)

描述每个方法,结构与字段表类似,但属性表中必须包含 Code 属性(存放字节码指令)。

method_info {
    u2             access_flags;     // ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED...
    u2             name_index;       // 方法名索引
    u2             descriptor_index; // 方法描述符索引
    u2             attributes_count;
    attribute_info attributes[];
}

方法描述符 = (参数类型)返回值类型。例如 (II)I 表示两个 int 参数返回 int。

2.8 属性表 (Attributes)

属性表是 Class 文件中最灵活的结构——它是一个可扩展的键值对集合,不同用途的信息都以"属性"的形式挂载。字段表、方法表、甚至 Class 文件本身都可以挂载属性表。

常见属性:

属性名挂载位置作用
Code方法表存放字节码指令、操作数栈深度、局部变量表大小
LineNumberTableCode 属性内字节码偏移量 → 源码行号的映射,调试用
LocalVariableTableCode 属性内局部变量名称和类型,调试用
StackMapTableCode 属性内字节码验证阶段的类型推导(JDK 6+)
Exceptions方法表方法声明的 checked 异常列表
SourceFileClass 文件源码文件名
InnerClassesClass 文件内部类信息
BootstrapMethodsClass 文件invokedynamic 引导方法(JDK 7+,lambda 的基础)

为什么属性表是最复杂的? 因为 Java 在不断演进,新特性往往需要新的元数据。如果把这些数据硬编码为固定字段,Class 文件格式每加一个新特性就得改规范。属性表这种可扩展的键值对机制,让 Class 文件格式能够在不破坏兼容性的前提下不断增长。例如 JDK 7 加的 BootstrapMethods、JDK 8 加的 MethodParameters,都是通过新增属性表实现的。

三、图解全景

一个简单类 Simple.java 的十六进制结构与偏移量标注:

Offset  Size  Field               Value          说明
──────  ────  ──────────────────  ─────────────  ───────────────────
0000    4     magic               0xCAFEBABE    JVM 魔数校验
0004    2     minor_version       0x0000        JDK 8 = major 52
0006    2     major_version       0x0034
0008    2     constant_pool_count 0x000D        #1-#12 (13个条目)
000A    N     constant_pool       ...           各种 CONSTANT_* 条目
──────       ──── ────────────────── ─────────  ──────────────────
00B0    2     access_flags        0x0021       ACC_PUBLIC + ACC_SUPER
00B2    2     this_class          #1           Simple
00B4    2     super_class         #2           java/lang/Object
00B6    2     interfaces_count    0x0000
00B8    N     interfaces[]        (空)
──────       ──── ────────────────── ─────────  ──────────────────
00C0    2     fields_count        0x0001
00C2    N     fields[]                        field_info { ACC_PRIVATE, "x", "I" }
──────       ──── ────────────────── ─────────  ──────────────────
00D0    2     methods_count       0x0002
00D2    N     methods[]           <init> + getX 每个方法含 Code 属性
──────       ──── ────────────────── ─────────  ──────────────────
0100    2     attributes_count    0x0001
0102    N     attributes[]        SourceFile: "Simple.java"

四、实战验证

编译并查看一个简单类:

bash
# 编译
javac Simple.java

# 反汇编完整输出
javap -v Simple.class
bash
# 十六进制查看前 4 字节
xxd Simple.class | head -1
# 输出: 00000000: cafe babe 0000 0034 ...  ← 直接看到魔数 + 版本号

反汇编输出逐字段标注(以 Simple.java 为例):

java
public class Simple {
    private int x;
    public Simple(int x) { this.x = x; }
    public int getX() { return x; }
}
Classfile /tmp/Simple.class                         ← SourceFile 属性
  Last modified 2020-01-01; size 280 bytes
public class Simple                                 ← 类头 + access_flags
  minor version: 0                                  ← 版本号
  major version: 52                                 ← major 52 = JDK 8
  flags: ACC_PUBLIC, ACC_SUPER                      ← 访问标志
Constant pool:                                      ← 常量池
   #1 = Methodref          #4.#13          // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#14          // Simple.x:I
   #3 = Class              #15             // Simple
   #4 = Class              #16             // java/lang/Object
   #5 = Utf8               "x"
   #6 = Utf8               "I"
   #7 = Utf8               "<init>"
   #8 = Utf8               "()V"
   #9 = Utf8               "Code"
  #10 = Utf8               "LineNumberTable"
  #11 = Utf8               "getX"
  #12 = Utf8               "()I"
  #13 = NameAndType        #7:#8           // "<init>":()V
  #14 = NameAndType        #5:#6           // "x":I
  #15 = Utf8               "Simple"
  #16 = Utf8               "java/lang/Object"
{
  private int x;                                   ← 字段表
    descriptor: I                                    // 字段描述符 int
    flags: ACC_PRIVATE

  public Simple(int);                              ← 构造函数 <init>
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:                                            ← Code 属性
      stack=2, locals=2, args_size=2
         0: aload_0
         1: invokespecial #1                // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iload_1
         6: putfield      #2                // Field x:I
         9: return
      LineNumberTable:                               ← 行号表
        line 3: 0
        line 4: 4
        line 5: 9

  public int getX();                               ← getX 方法
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                // Field x:I
         4: ireturn
}

五、面试视角

追问答案要点
常量池里都存什么?两大类:字面量(字符串、数值常量)和符号引用(类/接口、字段名、方法名、方法类型、方法句柄)。共 11+ 种 CONSTANT_* 类型
为什么属性表是最复杂的?可扩展设计——新特性通过新增属性而非修改结构实现。Code、LineNumberTable、StackMapTable、BootstrapMethods 等都是属性。属性表的存在让 Class 文件格式能向后兼容
魔数被篡改会怎样?JVM 第一道校验立即失败:java.lang.ClassFormatError: Incompatible magic value。行为类似于 损坏的图片被图片查看器拒绝
ACC_SUPER 标志有什么用?指示 invokespecial 指令使用增强语义。JDK 1.2 之后所有类都设置此标志,用于正确处理父类方法调用
描述符 [I[[Ljava/lang/Object; 表示什么?[I = int[] 一维数组,[[Ljava/lang/Object; = Object[][] 二维数组。纬度和类型紧密编码
符号引用和直接引用的区别?符号引用是字符串形式的类名/方法名/字段名(编译时确定),直接引用是内存地址/偏移量(运行时确定)。这种间接性使得动态链接和多态成为可能

📚 相关链接

  • **字节码指令集概览** — 字节码如何引用常量池
  • **实战javap反汇编分析** — 用 javap 实际查看 Class 结构
  • **运行时常量池** — 静态常量池 → 运行时常量池的转换
  • ← 返回 **字节码索引**

Knowledge4J — Java 知识库