字节码文件结构

345 阅读30分钟

字节码(Bytecode)是 Java 程序的一种中间表示形式,它是 Java 源代码经过编译器编译后生成的一种平台无关的指令集。字节码并不是直接由计算机硬件执行的机器语言,而是由 Java 虚拟机(JVM) 解释或编译成机器码并执行的。

JVM 在加载 .class 文件时,会将其解析为 ClassFile 结构,以便进行进一步的字节码验证、类加载和初始化等操作。

struct ClassFile {
    u4 magic;                        // 魔数,标识该文件为一个有效的 `.class` 文件
    u2 minor_version;                // 次版本号
    u2 major_version;                // 主版本号
    u2 constant_pool_count;          // 常量池的数量
    cp_info constant_pool[];        // 常量池,用于存储常量(类名、方法名、字符串等)
    u2 access_flags;                 // 类访问标志(public、abstract、final 等)
    u2 this_class;                   // 当前类在常量池中的索引
    u2 super_class;                  // 超类在常量池中的索引
    u2 interfaces_count;             // 接口数量
    u2 interfaces[];                 // 实现的接口
    u2 fields_count;                 // 字段数量
    field_info fields[];            // 字段表,描述类中的成员变量
    u2 methods_count;                // 方法数量
    method_info methods[];          // 方法表,描述类中的方法
    u2 attributes_count;             // 属性数量
    attribute_info attributes[];    // 属性表,描述类的额外信息,如源文件名、编译器信息等
};

参见 JVM规范

魔术数字 Magic Number

u4 magic;                        // 魔数,标识该文件为一个有效的 `.class` 文件

每个 .class 文件的前四个字节(u4)是一个 魔数 (0xCAFEBABE),用于标识该文件是有效的 Java 字节码文件。如果不匹配,JVM 就会认为文件不是合法的类文件。

The magic item supplies the magic number identifying the class file format; it has the value 0xCAFEBABE.

魔数并不是class文件独有的,它的作用是帮助程序识别文件的类型。

⨳ Windows 可执行文件 (.exe) 的魔数: 0x4D 0x5A0x4D 0x5A

⨳ PNG 图片 (.png) 的魔数: 0x89 0x50 0x4E 0x47 0x0D 0x0A 0x1A 0x0A

⨳ ZIP 压缩文件 (.zip) 的魔数: 0x50 0x4B 0x03 0x04

⨳ MP3 音频文件 (.mp3) 的魔数: 0x49 0x44 0x33(即 ID3

⨳ ...

可知,不同类型的文件有不同的魔数,魔数是用来唯一标识文件格式的,因此选择一个独特且不容易与其他常见文件格式重复的魔数非常重要。而且魔数通常位于文件的开头部分,方便程序在识别文件类型并决定如何处理文件。

至于为啥.class 文件的魔数是 CAFEBABE

这就要讲到 Java 之父——詹姆斯·高斯林(James Gosling)的趣闻了,高斯林在开发Java时经常去一个叫圣米歇尔巷(St Michael's Alley)的地方吃午餐,这个地方被称为“死亡咖啡”(Cafe Dead),于是他选择了 CAFEDEAD 作为对象持久化文件的魔数,而 BABE(宝贝)作为类文件的魔数。

版本信息 Version

u2 minor_version;                // 次版本号
u2 major_version;                // 主版本号

在魔数之后的四个字节(u2+u2)分别表示副版本号(Minor Version)和主版本号(Major Version)。

每次 Java 发布大版本,主版本加 1:比如 JDK 1.0 和 JDK 1.1 对应的主版本号为 45,JDK 1.2 是 46,JDK 1.3 是 47,...,以此类推,JDK 8.0 的版本号是 52,JDK 21 的版本号是 65 ...

可以使用 javap 工具来查看一个 .class 文件的详细信息,包括它的版本号。

javap -verbose MyClass.class
Classfile /path/to/MyClass.class 
    Last modified Jul 2, 2024; size 358 bytes 
    MD5 checksum 1234567890abcdef1234567890abcdef 
    Compiled from "MyClass.java" 
    major version: 52 
    minor version: 0 
    flags: (0x0000) 
    ...

其中,major version: 52minor version: 0 表示该 .class 文件是由 JDK 8 编译器(版本号 52.0)生成的。

常量池 Constant Pool

u2 constant_pool_count;   // 常量池的数量
cp_info constant_pool[];  // 常量池,用于存储常量(类名、方法名、字符串等)

每个 .class 文件都有一个自己的常量池(constant_pool)。常量池存储类、接口、字段、方法和字符串等常量数据。

The constant_pool is a table of structures (§4.4) representing various string constants, class and interface names, field names, and other constants that are referred to within the ClassFile structure and its substructures.

常量池(constant_pool)是一个结构体数组,大小由 constant_pool_count 指定,由于常量池的索引从 1 开始,因此 constant_pool_count 的值实际上是常量池数组的大小加 1。例如,如果常量池中有 10 个条目,那么 constant_pool_count 的值为 11。

The value of the constant_pool_count item is equal to the number of entries in the constant_pool table plus one.

常量池是一个由 cp_info 结构组成的数组,它根据常量的不同类型有不同的表示方式,JVM 目前一共定义了 20 种常量类型,这些常量名都以 "CONSTANT" 开头,以 "info" 结尾:

常量类型编号常量类型名描述
1CONSTANT_Utf8_info存储 UTF-8 编码的字符串常量
3CONSTANT_Integer_info存储 32 位的整型常量
4CONSTANT_Float_info存储 32 位的浮点型常量
5CONSTANT_Long_info存储 64 位的长整型常量
6CONSTANT_Double_info存储 64 位的双精度浮点型常量
7CONSTANT_Class_info存储类的符号引用
8CONSTANT_String_info存储字符串的符号引用
9CONSTANT_Fieldref_info存储字段的符号引用
10CONSTANT_Methodref_info存储方法的符号引用
11CONSTANT_InterfaceMethodref_info存储接口方法的符号引用
12CONSTANT_NameAndType_info存储字段或方法的名称和描述符的符号引用
15CONSTANT_MethodHandle_info存储方法句柄符号引用
16CONSTANT_MethodType_info存储方法类型符号引用
18CONSTANT_InvokeDynamic_info存储动态调用符号引用
19CONSTANT_Module_info存储动态调用符号引用
20CONSTANT_Package_info用于描述包的信息

整数、小数、字符串

CONSTANT_Integer_info 整型

CONSTANT_Integer_info { 
    u1 tag; // 类型标识符,值为 3,表示这是一个整数常量 
    u4 bytes; // 4 字节,存储一个整数值 
}

CONSTANT_Integer_info,用于表示一个 32 位的整数常量,它的结构是一个 4 字节的整数值。如:

int a = 42;

编译后,生成的 .class 文件的常量池中,就会包含一个 CONSTANT_Integer_info 条目,表示值 42

CONSTANT_Integer_info { 
    tag = 3   // 表示整数常量
    bytes = 42  // 存储值 42
}

CONSTANT_Long_info 长整型

CONSTANT_Long_info { 
    u1 tag;  // 类型标识符,值为 5,表示这是一个长整型常量 
    u4 high_bytes; // 存储 64 位长整型值的高 4 字节 
    u4 low_bytes; // 存储 64 位长整型值的低 4 字节 
}

CONSTANT_Long_info 由两个 4 字节的部分组成,共 8 字节,用来存储一个 64 位的整数。如:

long a = 9223372036854775807L;

在常量池中,该常量会以下面形式存储:

CONSTANT_Long_info { 
    tag = 5 // 5 表示长整型常量 
    high_bytes = 0x7FFF_FFFF // 9223372036854775807 的高 4 字节 
    low_bytes = 0xFFFFFFFF // 9223372036854775807 的低 4 字节 
}

需要注意的是 由于 long 类型是 64 位的,一个CONSTANT_Long_info类型的常量占用 2 个常量池条目,每个条目都是一个 CONSTANT_Long_info 类型,分别存储高 32 位和低 32 位。

CONSTANT_Float_info 浮点型

CONSTANT_Float_info {
    u1 tag;      // 常量类型标识符,值为 4
    u4 bytes;    // 存储浮点数的 4 字节数据
}

CONSTANT_Float_info 用于表示一个 32 位的单精度浮点数常量。如:

float f = 3.14f;

在常量池中,该常量会以下面形式存储:

CONSTANT_Float_info {
    tag = 4                // 标识为 FLOAT 类型
    bytes = 0x4048F5C3     // 32 位浮点数的值
}

3.14 的 IEEE 754 32 位单精度浮点数表示为 0 10000000 10010001111010111000011,即 0x4048F5C3(16 进制表示)。

CONSTANT_Double_info 双浮点型

CONSTANT_Double_info { 
    u1 tag; // 常量类型标识符,值为 6 
    u8 bytes; // 存储双精度浮点数的 8 字节数据 
}

CONSTANT_Double_info 用于表示一个 64 位的双精度浮点数常量(double 类型),如:

double d = 3.141592653589793;

在常量池中,该常量会以下面形式存储:

CONSTANT_Double_info {
    tag = 6                // 标识为 DOUBLE 类型
    high_bytes = 0x400921FB  // 双精度浮点数的高 4 字节
    low_bytes = 0x54442D18   // 双精度浮点数的低 4 字节
}

long 类型一样,CONSTANT_Double_info 类型的常量也会占用 2 个常量池条目

CONSTANT_Utf8_info Utf8字面量

CONSTANT_Utf8_info { 
    u1 tag; // 常量类型标识符,值为 1 
    u2 length; // UTF-8 字符串的长度(以字节为单位)
    u1 bytes[length]; // 字符串的内容(UTF-8 编码)
}

CONSTANT_Utf8_info 用于表示 UTF-8 编码的字符串常量。如:

String str = "Hello, World!";

在常量池中,该常量会以下面形式存储:

CONSTANT_Utf8_info {
    tag = 1                  // 标识为 UTF-8 字符串类型
    length = 13              // 字符串长度为 13 字节
    bytes = 0x48 0x65 0x6C 0x6C 0x6F 0x2C 0x20 0x57 0x6F 0x72 0x6C 0x64 0x21 // UTF-8 编码的 "Hello, World!"                 
}

你可能会问,为啥字符串会起个 CONSTANT_Utf8_info 名,为什么不叫 CONSTANT_String_info,别急,往下看就明白了。

对 Utf8 的引用

CONSTANT_String_info 字符串

CONSTANT_String_info { 
    u1 tag; // 常量类型标识符,值为 8 
    u2 string_index; // 指向常量池中 `CONSTANT_Utf8_info` 的索引
}

CONSTANT_String_info 用于表示字节码中的字符串常量。它的作用是引用一个实际存储字符串内容的 CONSTANT_Utf8_info 项。如:

String MESSAGE = "Hello";

在常量池中,该常量可能会以下面形式存储:

Constant Pool:
----------------------------------------
| 1  | CONSTANT_Utf8_info | "Hello" |  
----------------------------------------
| 2  | CONSTANT_String_info | string_index -> 1 |  
----------------------------------------
  • CONSTANT_Utf8_info(索引为 1)存储了字符串 "Hello" 的 UTF-8 编码。
  • CONSTANT_String_info(索引为 2)存储了一个索引 1,它指向了 CONSTANT_Utf8_info 项,表示 MESSAGE 变量的字符串常量 "Hello"

CONSTANT_Class_info 类或接口

CONSTANT_Class_info { 
    u1 tag;  // 常量类型标识符,值为 7 
    u2 name_index; // 指向常量池中 `CONSTANT_Utf8_info` 的索引
}

CONSTANT_Class_info 于表示类或接口的符号引用。它指向一个类或接口的全限定名,通常用于描述类的名称或接口的名称。如:

private String name;

在常量池中,该常量可能会以下面形式存储:

Constant Pool:
----------------------------------------
| 1  | CONSTANT_Utf8_info   | "java/lang/String" |
----------------------------------------
| 2  | CONSTANT_Class_info   | name_index -> 1         |
----------------------------------------
  • CONSTANT_Utf8_info(索引为 1)存储了类 String 的全限定名 java/lang/String
  • CONSTANT_Class_info(索引为 2)存储了一个指向常量池中 CONSTANT_Utf8_info 项的索引 1,即 String 类的符号引用。

CONSTANT_Class_info 存储的是类或接口的全限定名(例如 java/lang/String),这种引用通常需要在运行时解析为实际的类对象,这也就是所谓的“符号引用”。

CONSTANT_NameAndType_info 方法或字段的名称和描述符

CONSTANT_NameAndType_info { 
    u1 tag;  // 常量类型标识符,值为 12 
    u2 name_index; // 指向常量池中 `CONSTANT_Utf8_info` 的索引
    u2 descriptor_index; // 指向常量池中 `CONSTANT_Utf8_info` 的索引
}

CONSTANT_NameAndType_info 用于描述方法或字段的名称和描述符(即方法签名或字段类型),其中 name_index 指向常量池中的 CONSTANT_Utf8_info 项,存储了字段或方法的名称;descriptor_index 也指向常量池中的 CONSTANT_Utf8_info 项,存储了字段或方法的描述符(如方法的返回类型及参数类型)。如:

public class Example {
    public int add(int a, int b) {
        return a + b;
    }
}

在常量池中,该常量可能会以下面形式存储:

Constant Pool:
----------------------------------------
| 1  | CONSTANT_Utf8_info   | "add"          |  // 方法名
----------------------------------------
| 2  | CONSTANT_Utf8_info   | "(II)I"        |  // 方法描述符
----------------------------------------
| 3  | CONSTANT_NameAndType_info | name_index -> 1, descriptor_index -> 2 |  // 方法的名称和描述符
----------------------------------------
  • CONSTANT_Utf8_info(索引为 1)存储了方法名 add
  • CONSTANT_Utf8_info(索引为 2)存储了方法描述符 (II)I,表示 add 方法有两个 int 类型的参数,返回一个 int 类型的值;
  • CONSTANT_NameAndType_info(索引为 3)将方法名 add 和方法描述符 (II)I 关联起来,作为一个整体来表示方法。

看到这应该明白了吧,CONSTANT_Utf8_info 不仅仅存储用双引号括起来的字符串,还存储在 .class 文件中的所有可见字符串类型的数据。这包括类名、接口名、方法名、字段名、描述符、修饰符等所有需要表示的字符串信息。

对 Utf8 引用的引用

CONSTANT_Fieldref_info 字段

CONSTANT_Fieldref_info { 
    u1 tag;  // 常量类型标识符,值为 9
    u2 class_index;  // 指向常量池中的 `CONSTANT_Class_info` 项,表示该字段所在的类(或接口)的符号引用。
    u2 name_and_type_index;  // 指向常量池中的 `CONSTANT_NameAndType_info` 项,表示字段的名称和描述符。
} 

CONSTANT_Fieldref_info 表示对字段的引用,如下:

public class Example {
    private String name;
}

在常量池中,该常量可能会以下面形式存储:

Constant Pool:
----------------------------------------
| 1  | CONSTANT_Utf8_info   | "java/lang/String"  |  // String 类的全限定名
----------------------------------------
| 2  | CONSTANT_Class_info   | index -> 1          |  // Class 信息,引用 String 类
----------------------------------------
| 3  | CONSTANT_Utf8_info   | "name"              |  // 字段 name 的名称
----------------------------------------
| 4  | CONSTANT_Utf8_info   | "Ljava/lang/String;" |  // 字段 name 的类型描述符
----------------------------------------
| 5  | CONSTANT_NameAndType_info | name_index -> 3, descriptor_index -> 4 |  // 字段的名称和描述符
----------------------------------------
| 6  | CONSTANT_Fieldref_info | class_index -> 2, name_and_type_index -> 5 |  // 字段 name 的引用
----------------------------------------

会好理解吧,CONSTANT_Fieldref_info 是对引用字符串类型 CONSTANT_Class_info 和 CONSTANT_NameAndType_info 的引用。

CONSTANT_Methodref_info 方法

CONSTANT_Methodref_info { 
    u1 tag; // 常量类型标识符,值为 10
    u2 class_index; // 指向常量池中的 `CONSTANT_Class_info` 项,表示该方法所在的类或接口。
    u2 name_and_type_index; // 指向常量池中的 `CONSTANT_NameAndType_info` 项,表示方法的名称和描述符。
} 

CONSTANT_Methodref_info用于表示对方法的引用(符号引用)。这个结构和 CONSTANT_Fieldref_info 一模一样,就不赘述了,贴一个关于方法编译后的常量池,

Constant Pool:
----------------------------------------------
| 1  | CONSTANT_Utf8_info   | "Example"       |  // 类名
----------------------------------------------
| 2  | CONSTANT_Class_info   | class_index -> 1 |  // Class 信息,指向 Example 类
----------------------------------------------
| 3  | CONSTANT_Utf8_info    | "exampleMethod"  |  // 方法名
----------------------------------------------
| 4  | CONSTANT_Utf8_info    | "()V"            |  // 方法描述符,表示无参数且返回 void
----------------------------------------------
| 5  | CONSTANT_NameAndType_info | name_index -> 3, descriptor_index -> 4 |  // 方法的名称和描述符
----------------------------------------------
| 6  | CONSTANT_Methodref_info | class_index -> 2, name_and_type_index -> 5 |  // 方法引用
----------------------------------------------

应该能通过常量池反推编译前的 Java方法 吧。

CONSTANT_InterfaceMethodref_info 接口方法

CONSTANT_InterfaceMethodref_info { 
    u1 tag; // 常量类型标识符,值为 11
    u2 class_index; // 指向常量池中的 `CONSTANT_Class_info` 项,表示接口所在的类。
    u2 name_and_type_index; // 指向常量池中的 `CONSTANT_NameAndType_info` 项,表示方法的名称和描述符。
} 

CONSTANT_InterfaceMethodref_info 表示对接口方法的符号引用。与 CONSTANT_Methodref_info 类似,就不赘述了。

字节码中的 invokeinterface 指令会使用 CONSTANT_InterfaceMethodref_info 来调用接口方法,而 invokestaticinvokevirtual 等指令会使用 CONSTANT_Methodref_info 来调用类方法。

动态调用相关

从 JDK1.7 开始,为了更好的支持动态语言调用,新增了 3 种常量池类型(CONSTANT_MethodType_infoCONSTANT_MethodHandle_infoCONSTANT_InvokeDynamic_info)。它们是支持 invokedynamic 指令和动态语言调用的核心组成部分,关于这部分放到动态指令那一节进行讲解

常量池后面跟着的是访问标志。

访问标志 Access Flags

u2 access_flags;                 // 类访问标志(public、abstract、final 等)

访问标志 描述了该类或接口的访问级别、是否是抽象类、是否是最终类等特性。

The value of the access_flags item is a mask of flags used to denote access permissions to and properties of this class or interface.

访问标志是一个 16 位(2 字节)的位掩码(bitmask),它通过不同的标志位组合来表示不同的属性。目前只使用了其中的 9 个位。

访问标志对应值描述
ACC_PUBLIC0x0001表示该类是公共的,任何地方都可以访问。如果没有设置该标志,则该类在当前包内可见,其他包中的类无法访问。
ACC_FINAL0x0010表示该类是 final,不能被继承。如果一个类被声明为 final,则不允许创建它的子类。
ACC_SUPER0x0020表示该类需要调用父类的方法时,是否使用 invokespecial 指令。这个标志主要用于 Java 虚拟机内部,在 JDK 8 和后续版本中,无论 ACC_SUPER 标志的实际值是什么,JVM 都假定每个类文件都设置了 ACC_SUPER 标志。
ACC_INTERFACE0x0200表示该类是一个接口,而不是普通的类。接口的特殊性在于它不能包含实例字段和方法的实现(直到 Java 8 引入接口默认方法)。
ACC_PUBLIC0x0001表示该类是公共的,任何地方都可以访问。如果没有设置该标志,则该类在当前包内可见,其他包中的类无法访问。
ACC_ABSTRACT0x0400表示该类是 abstract,即它是抽象类,不能直接实例化。抽象类通常包含抽象方法,这些方法没有实现,必须在子类中实现。
ACC_SYNTHETIC0x1000表示该类是合成的(由编译器生成),而非由程序员直接编写的,未出现在源代码中。
ACC_ANNOTATION0x2000表示该类是一个注解类型。
ACC_ENUM0x4000表示该类是一个 enum 类型。
ACC_MODULE0x8000表示该类是一个模块,而不是一个类或接口。

假设我们有一个类的访问标志为 0x0021,其二进制形式为 0000 0000 0010 0001。通过位掩码来解读,就知道该类是一个普通的公共类,但既不是抽象类,也不是接口,也不是 final 类。

类与接口索引 Class And Interface

this_class 类索引

u2 this_class;                   // 当前类在常量池中的索引

this_class 表示该类的索引,它是一个指向常量池中类引用(CONSTANT_Class_info)的索引,标识当前 .class 文件所定义的类。

The value of the this_class item must be a valid index into the constant_pool table. The constant_pool entry at that index must be a CONSTANT_Class_info structure (§4.4.1) representing the class or interface defined by this class file.

如一个简单的类:

public class MyClass {
}

在常量池中,该常量可能会以下面形式存储:

Constant Pool:
----------------------------------------
| 1  | CONSTANT_Utf8_info   | "MyClass"  |  // MyClass 类的全限定名
----------------------------------------
| 2  | CONSTANT_Class_info   | name_index -> 1  |  // Class 信息,引用 MyClass 类
----------------------------------------

假设 MyClass 在常量池中的索引为 02,那么 this_class 的值就会是 02

this_class 的存在使得 JVM 可以识别当前 .class 文件中定义的是哪个类。当 .class 文件被加载到 JVM 中时,JVM 会读取 this_class 字段的值。this_class 指向常量池中的 CONSTANT_Class_info,JVM 然后可以从 CONSTANT_Class_info 获取类的全限定名。

super_class 父类索引

u2 super_class;                  // 超类在常量池中的索引

父类索引是一个指向常量池中保存父类全限定类名的 CONSTANT_Class_info 项。

For a class, the value of the super_class item either must be zero or must be a valid index into the constant_pool table.If the value of the super_class item is nonzero, the constant_pool entry at that index must be a CONSTANT_Class_info structure representing the direct superclass of the class defined by this class file.

比如:

public class MyClass extends MySuperClass {
}

如果编译后的常量池包含以下内容:

常量池:
----------------------------------------
| 1  | CONSTANT_Utf8_info   | "MyClass"  |  // 当前类名 MyClass
----------------------------------------
| 2  | CONSTANT_Utf8_info   | "MySuperClass" |  // 父类名 MySuperClass
----------------------------------------
| 3  | CONSTANT_Class_info  | name_index -> 1    |  // 当前类 MyClass
----------------------------------------
| 4  | CONSTANT_Class_info  | name_index -> 2    |  // 父类 MySuperClass
----------------------------------------

那么 this_class 的值是 03super_class 的值是 04

如果当前类没有显式指定父类(即没有继承任何类),那么 super_class 字段的值为 00,表示它隐式继承自 java.lang.Object 类,因为常量池中的数据项是从 1 开始的。

If the value of the super_class item is zero, then this class file must represent the class Object, the only class or interface without a direct superclass.

interfaces 接口索引表

u2 interfaces_count;             // 接口数量
u2 interfaces[];                 // 实现的接口

interfaces_count 2 字节(u2),它包含该类实现的接口数量。

后面紧跟着的 interfaces 为一组索引值,每个索引指向常量池中的一个接口的条目。

Each value in the interfaces array must be a valid index into the constant_pool table.

接口索引和 this_classsuper_class 差不多吧,只是 Java 是 单继承 的语言,只能直接继承一个父类,所以 super_class 不是数组,而 Java 同时又允许一个类实现多个接口,所以需要 interfaces 数组,来保存指向常量池中多个接口类(CONSTANT_Class_info)的索引。

字段表 Fields

u2 fields_count;        // 字段数量
field_info fields[];    // 字段表,描述类中的成员变量

fields_count 描述类中成员变量(字段)的数量。

后面紧跟着的 fields 则描述了类中定义的所有字段。

The field_info structures represent all fields, both class variables and instance variables, declared by this class or interface type.

每个字段都由一个 field_info 结构表示,包含字段的名称、类型和访问标志:

struct field_info {
    u2 access_flags;       // 字段的访问标志(如 public, private, static 等)
    u2 name_index;         // 常量池索引,指向字段的名称
    u2 descriptor_index;   // 常量池索引,指向字段的类型描述符
    u2 attributes_count;   // 字段的属性数量
    attribute_info attributes[]; // 字段的属性表
};

access_flags 字段访问标记

field_info 里面的 access_flags 是表示字段的访问标志,和类的访问标识一样,也是一个16 位(2 字节)的位掩码,不同的位表示不同的含义

访问标志对应值描述
ACC_PUBLIC0x0001字段是公共的(public),可以在任何地方访问。
ACC_PRIVATE0x0002字段是私有的(private),仅在当前类内部访问。
ACC_PROTECTED0x0004字段是受保护的(protected),可以在同一包中的类和所有子类中访问。
ACC_STATIC0x0008字段是静态的(static),属于类本身而不是类的实例。
ACC_FINAL0x0010字段是常量(final),必须在声明时或构造函数中初始化,一旦赋值后就不能再改变。
ACC_VOLATILE0x0040字段是易变的(volatile),意味着字段的值可能随时被其他线程修改,需要告知 JVM 和编译器该字段的值不应被缓存。
ACC_TRANSIENT0x0080字段是瞬态的(transient),在序列化时不应包含在内。
ACC_SYNTHETIC0x0001字段是合成的,由编译器生成的字段,而非源代码中显式声明的字段。
ACC_ENUM0x0001字段是一个枚举常量。

很好理解吧假,假设有以下字段:

public final static int MAX_VALUE = 100;

ACC_PUBLIC 是 0x0001,ACC_FINAL 是 0x0010,ACC_STATIC 是 0x0008,所以该字段的 access_flags 就是 0x0001 | 0x0010 | 0x0008 = 0x0019

descriptor_index 类型描述符

类型描述符定义了字段的数据类型,例如 intString 等。字段的类型描述符遵循一种特殊的语法规则,大致如下:

基本数据类型:每种基本类型对应一个字符:

  • 首字母:byte 对应 B; char 对应 C ; double 对应 D; float 对应 F ; int 对应 I; short 对应 S
  • 无规则:long 对应 S ; boolean 对应 Z 。(L 被对象占了,B 被 byte 占了)

对象类型:对象类型(类、接口)的描述符是以 L 开头,后跟类的全限定名,最后以 ; 结束。例如:

  • Ljava/lang/String;:表示 String 类型
  • Lcom/example/Person;:表示 Person 类型

数组类型:数组类型描述符是由一个或多个 [ 开头,后跟类型描述符。例如:

  • [I:表示 int[] 数组
  • [[Ljava/lang/String;:表示 String[][] 数组

name_index 字段的名称

name_index 也是一个指向常量池中的一个索引值,表示字段的名称

假如有以下 Java 类:

public class Example {
    private int value;
    private String message;
}

编译后的字节码中的常量池部分可能如下:

Constant Pool:
----------------------------------------
| #1  | CONSTANT_Utf8_info  | "value"     | // 字段名称
----------------------------------------
| #2  | CONSTANT_Utf8_info  | "I"         | // int 类型描述符
----------------------------------------
| #3  | CONSTANT_Utf8_info  | "message"   | // 字段名称
----------------------------------------
| #4  | CONSTANT_Utf8_info  | "Ljava/lang/String;" | // String 类型描述符
----------------------------------------

对应的字段表 (fields) 就是:

fields_count: 2
fields:
----------------------------------------
| access_flags       | 0x0002 (private) |
| name_index         | 1                | // 指向常量池中的 "value"
| descriptor_index   | 2                | // 指向常量池中的 "I"
| attributes_count   | 0                | // 没有其他属性
----------------------------------------
| access_flags       | 0x0002 (private) |
| name_index         | 3                | // 指向常量池中的 "message"
| descriptor_index   | 4                | // 指向常量池中的 "Ljava/lang/String;"
| attributes_count   | 0                | // 没有其他属性
----------------------------------------

字段的三要素 访问标志类型修饰符名称。齐活了,至于 field_info 里面的属性 attributes 是啥,放到后面一起介绍哈。

方法表 Methods

u2 methods_count;                // 方法数量
method_info methods[];          // 方法表,描述类中的方法

methods_count 表示该类中方法的总数,它的值决定了 methods[] 数组中有多少个 method_info 结构体。

重点是 method_info 结构体:

struct method_info {
    u2 access_flags;          // 访问标志
    u2 name_index;            // 方法名称索引
    u2 descriptor_index;      // 方法描述符索引
    u2 attributes_count;      // 属性数
    attribute_info attributes[]; // 属性列表
};

是不是和字段表 Fields 差不多。

access_flags 方法访问标志

方法访问标志比字段访问标志多一点。

ACC_PUBLIC (0x0001)、 ACC_PRIVATE (0x0002)、ACC_PROTECTED(0x0004)、ACC_STATIC (0x0008)、ACC_FINAL (0x0010)、ACC_SYNTHETIC (0x1000) 这几个和字段访问标志一样,下面看几个不一样的:

访问标志对应值描述
ACC_SYNCHRONIZED0x0020标识方法为 synchronized,方法在执行时会对其加锁,保证同一时间只能有一个线程执行此方法。
ACC_BRIDGE0x0040标识该方法是一个 桥接方法。桥接方法通常是由编译器自动生成的,主要用于解决泛型类型擦除后生成的转换方法。比如,在泛型中有类型擦除的情况,JVM 会自动生成桥接方法来保证方法签名的兼容性。
ACC_VARARGS0x0080标识该方法接受可变参数(varargs)。这是在方法签名中指定一个变长参数列表(如 ...)。
ACC_NATIVE0x0100标识该方法为 native,表示该方法的实现是由非 Java 代码提供的,通常是通过 JNI(Java Native Interface)与 C/C++ 代码进行交互。
ACC_ABSTRACT0x0400标识方法为 abstract,表示该方法没有方法体,只是一个方法声明,必须由子类提供具体实现。
ACC_STRICT0x0800标识该方法为 strictfp,表示该方法的浮点计算遵循严格的浮点计算规则,保证跨平台的一致性。

descriptor_index 方法描述符

与字段的描述符不同的是,方法描述符包括参数类型+返回值类型。

但看参数类型和返回值类型规则和字段的标识符规则一样,区别就是组合起来的格式为:

(参数类型参数类型...)返回值类型

如:

List<String> myMethod(Map<String, Integer> map, boolean flag);
  • 参数类型为 Map<String, Integer>boolean,对应的描述符为 (Ljava/util/Map;Z)

  • 返回值类型为 List<String>,对应的描述符为 Ljava/util/List;

组合起来完整描述符为:(Ljava/util/Map;Z)Ljava/util/List;

方法描述符和字段描述符还有一点不同就是方法的参数或方法的返回值类型为空(void)对应的描述符为 V

name_index 方法的名称

和字段的 name_index 一样,方法的 name_index 也是一个指向常量池 CONSTANT_Utf8_info 项的索引。

字段的三要素 访问标志参数、返回值类型修饰符名称,齐活了,下面看一下个简单案例:

public class MyClass {
    public int add(int a, int b) {
        return a + b;
    }
    
    public void printHello() {
        System.out.println("Hello");
    }
}

为了简化,我们只关注 addprintHello 方法的名称及描述符。常量池可能如下:

索引类型内容
1CONSTANT_Utf8_info"add"
2CONSTANT_Utf8_info"printHello"
3CONSTANT_Utf8_info"(II)I"
4CONSTANT_Utf8_info"(V)V"

对于 MyClass 类,它的 methods[] 部分可能是这样的:

methods_count = 2;  // 有两个方法
methods[] = {
    // Method 1: add(int a, int b)
    {
        access_flags = 0x0001;   // public
        name_index = 1;          // 指向常量池中的 "add"
        descriptor_index = 3;    // 指向常量池中的 "(II)I"
    },
    // Method 2: printHello()
    {
        access_flags = 0x0001;   // public
        name_index = 2;          // 指向常量池中的 "printHello"
        descriptor_index = 4;    // 指向常量池中的 "(V)V"
    }
}

方法表也有属性,下面就是统一看一下属性是什么。

属性表 Attributes

u2 attributes_count;             // 属性数量
attribute_info attributes[];    // 属性表,描述类的额外信息,如源文件名、编译器信

属性表Attributes)是用来存储与类、字段、方法和代码相关的额外信息。每个类、字段或方法都可以有多个属性,这些属性帮助 JVM 解释类的行为和提供额外的元数据。

属性表的设计非常灵活。Java虚拟机规范(JVM Specification)允许开发者或编译器向 .class 文件的属性表中添加自定义的属性,只要这些属性的名称与已有的标准属性名称不重复。

预定义属性:根据 Java 虚拟机规范,JVM 会理解和处理一些标准属性。这些预定义属性对于所有的 Java 虚拟机实现都是必须识别和支持的。

自定义属性:除了预定义属性外,开发者或编译器可以根据需要自定义额外的属性。JVM 会忽略不认识的自定义属性,因此,添加新的属性不会破坏现有的程序和工具。这种方式为编译器提供了扩展能力,例如可以存储调试信息、代码覆盖率等额外的信息。

最初,Java 虚拟机规范只定义了少量的预定义属性,但是随着 Java 版本的发展和语言功能的增加,JVM 规范对预定义属性的要求逐渐增加。在 Java SE 12 版本中,预定义属性的数量已经增加到 29项

下面就挑几个常见的进行介绍:

ConstantValue 常量值

ConstantValue_attribute { 
    u2 attribute_name_index;      // 属性名索引,指向常量池中的"ConstantValue"字符串
    u4 attribute_length;          // 属性长度,ConstantValue 属性的 attribute_length 值恒定为 2
    u2 constantvalue_index;       // 常量值的索引,指向常量池中的常量
}

ConstantValue 属性用于描述一个常量字段的值。它只出现在字段的 field_info 中,并且仅适用于 staticfinal 修饰的字段。这些字段的值在编译时就已经确定,因此它们被称为常量(constant)。

假设有一个 Java 类,它包含一个 static final 常量字段:

public class MyClass {
    public static final double PI = 3.14159;
}

在字节码中,这个常量字段的 ConstantValue 属性可能会类似于以下结构:

Constant Pool:
----------------------------------------
| 1  | CONSTANT_Utf8_info   | "MyClass"       |
| 2  | CONSTANT_Utf8_info   | "PI"            |
| 3  | CONSTANT_Utf8_info   | "D"             |
| 4  | CONSTANT_Double_info | 3.14159         |
| 5  | CONSTANT_Utf8_info | "ConstantValue"    |
----------------------------------------

Field Info:
----------------------------------------
| Access Flags | Name Index | Descriptor Index | Attribute Count |
| 0x0009       | 2          | 3                | 1               |
----------------------------------------

Attributes:
----------------------------------------
| Attribute Name Index | Attribute Length | ConstantValue Index |
| 5                    | 2                | 4                   |
----------------------------------------

注意 attribute_name_index 恒指向 UTF8常量项 "ConstantValue"attribute_length 恒定为 2

属性表描述了与类、字段、方法等相关的附加信息。例如,源文件的名称、调试信息、异常表等都可以存储在属性表中。常见的属性包括: 。

Code 方法字节码

每个方法都有一个 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[exception_table_length];// 异常表 
    u2 attributes_count; // 其他属性的数量 
    attribute_info attributes[]; // 其他属性
}

如一个简单的加法方法:

public int add(int a, int b) {
    return a + b;
}

编译后可能为:

  public int add(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: ireturn
      LineNumberTable:
        line 5: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  this   Lcom/cangoking/bytecodes/CodeDemo;
            0       4     1     a   I
            0       4     2     b   I
  • stack=2:栈的最大深度是 2。这意味着在方法执行过程中,最多同时有 2 个操作数在栈上。

  • locals=3:局部变量表的大小是 3。这表示方法有 3 个局部变量。

  • args_size=3:方法的参数大小是 3。即,方法有 3 个参数,包括 this(对象引用)、ab

  • LineNumberTable:行号表,这是Code的属性附加表,表示字节码与源代码行号之间的映射。这里的含义是:字节码的第 0 行对应源代码中的第 5 行。

  • LocalVariableTable:局部变量表,这也是Code的附加属性表,其中 Start表示局部变量的起始字节码位置;Length 表示局部变量的有效范围(从起始位置开始的字节码长度);Slot表示局部变量表中的索引;Name 表示局部变量的名称;Signature表示局部变量的类型签名。

  • ExceptionTable:异常表,用于描述方法中异常处理器的范围,以及在特定范围内捕获的异常类型。因 add 方法无异常处理逻辑,则无异常表。异常表也很好理解如下:

fromtotargettype
048Class java/lang/ArithmeticException

表示在 04 的字节码范围内,如果发生 ArithmeticException,会跳转到偏移量 8 的异常处理器。

总结

到此为止,class 文件的结构大致都介绍完了。是不是都看吐了 -_-||

可以看到 class 文件最重要的就是常量池部分,以及用常量项描述的类、接口信息,字段信息和方法信息。