一、编译的结果
在操作系统课程中,我们通常会学到:程序最终会被编译器翻译成由 0 和 1 组成的 二进制格式,并交给计算机执行。
不过,随着虚拟机技术的发展,以及大量运行在虚拟机之上的编程语言的出现,程序编译后的结果已经不再只有“本地机器码”这一种形式。
也就是说,源代码并不一定要直接编译成某个操作系统、某种 CPU 指令集专属的机器码。越来越多的语言会选择一种与具体操作系统、硬件平台无关的中间格式,作为程序编译后的存储格式。
Java 的 .class 文件就是这样一种平台无关的中间格式。
Java 源代码
↓ javac 编译
Class 字节码文件
↓ JVM 加载、验证、解释执行或 JIT 编译
机器执行
因此,.class 文件是 Java 实现“一次编译,到处运行”的重要基础。
二、Class 文件是什么?
任何一个 Class 文件,通常都对应着唯一的一个类或接口的定义信息。
不过需要注意的是,类或接口并不一定非要以磁盘文件的形式存在。它们也可以在运行期间被动态生成,然后直接交给类加载器加载。
例如,下面这些场景都可能动态生成 Class 字节码:
动态代理
CGLIB
ASM
Javassist
Lambda 表达式相关机制
运行时字节码增强
所以,Class 文件本质上不是“磁盘上的某个文件”这么简单,而是一种 JVM 规定好的 二进制数据格式。
只要一段二进制数据满足 JVM 对 Class 文件格式的要求,它就可以被类加载器加载。
三、Class 文件的基本存储规则
Class 文件是一组以 8 位字节为基础单位的二进制流。
各个数据项会严格按照 JVM 规范规定的顺序,紧凑地排列在文件中。中间没有额外的分隔符,也没有空隙。
根据《Java 虚拟机规范》的定义,Class 文件格式采用一种类似于 C 语言结构体的伪结构来描述。
这种伪结构中只有两种基本数据类型:
无符号数
表
1. 无符号数
无符号数属于基本数据类型。
JVM 规范使用:
u1
u2
u4
u8
分别表示:
| 类型 | 含义 |
|---|---|
u1 | 1 个字节的无符号数 |
u2 | 2 个字节的无符号数 |
u4 | 4 个字节的无符号数 |
u8 | 8 个字节的无符号数 |
无符号数可以用来描述:
数字
索引引用
数量值
按照 UTF-8 编码构成的字符串值
例如,constant_pool_count 是一个 u2 类型的数据,用来表示常量池容量计数值。
2. 表
表是由多个无符号数或者其他表作为数据项构成的复合数据类型。
为了便于区分,JVM 规范中的表通常会以 _info 结尾,例如:
cp_info
field_info
method_info
attribute_info
表通常用于描述具有层次关系的复合结构。
整个 Class 文件本身,也可以看作是一张大表。
3. 数量不确定的数据如何表示?
在 Class 文件中,经常会出现“同一类型的数据有多个,但数量不固定”的情况。
例如:
常量池中有多少个常量?
类实现了多少个接口?
类中有多少个字段?
类中有多少个方法?
方法中有多少个属性?
对于这种情况,Class 文件通常采用下面这种结构:
数量计数器 + 若干个连续的数据项
例如:
fields_count
fields[fields_count]
意思是:
先用 fields_count 表示字段数量
然后紧跟着 fields_count 个 field_info 结构
这种设计使得 Class 文件既能保持紧凑,又能描述数量不固定的数据结构。
四、Class 文件整体结构
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]; // 属性表
}
可以先把它简化理解为:
Class 文件
├── 魔数
├── 版本号
├── 常量池
├── 类的访问标志
├── 当前类
├── 父类
├── 接口表
├── 字段表
├── 方法表
└── 属性表
其中最重要的部分是:
常量池
字段表
方法表
属性表
尤其是 常量池 和 Code 属性,它们是理解字节码执行机制的关键。
五、魔数和 Class 文件版本
1. 魔数
魔数,也叫 Magic Number,指的是文件格式中用于标识文件类型的固定字节序列。
它通常位于文件开头,用来帮助程序快速判断当前文件属于哪种格式。
每个 Class 文件的前 4 个字节固定为:
0xCAFEBABE
这 4 个字节就是 Class 文件的魔数。
它的作用是告诉 JVM:
当前文件是否是一个合法的 Class 文件。
如果 JVM 读取到一个文件,发现文件开头不是 0xCAFEBABE,就会认为它不是合法的 Class 文件。
2. Class 文件版本号
紧接着魔数之后的 4 个字节表示 Class 文件版本号。
第 5、6 个字节:minor_version,次版本号
第 7、8 个字节:major_version,主版本号
其中主版本号更常用,它决定了当前 Class 文件需要哪个版本的 JVM 才能运行。
例如:
| Java 版本 | major_version |
|---|---|
| Java 8 | 52 |
| Java 11 | 55 |
| Java 17 | 61 |
| Java 21 | 65 |
如果使用低版本 JVM 去运行高版本编译出来的 Class 文件,就可能出现:
UnsupportedClassVersionError
例如,用 Java 8 的 JVM 去运行 Java 17 编译出来的 .class 文件,就可能出现该错误。
六、常量池
在主次版本号之后,紧接着就是常量池入口。
常量池可以理解为 Class文件中的 资源仓库。
它是 Class 文件结构中与其他项目关联最多的数据区域,通常也是占用 Class 文件空间最大的数据项目之一。
同时,常量池也是 Class 文件中第一个出现的表类型数据项目。
1. 常量池容量计数器
由于常量池中的常量数量是不固定的,所以在常量池正式开始之前,会有一个 u2 类型的数据:
constant_pool_count
它表示常量池的容量计数值。
需要注意:
常量池索引从 1 开始,而不是从 0 开始。
因此,如果 constant_pool_count = 26,并不表示常量池中有 26 个有效常量项,而是表示有效索引范围通常是:
1 ~ 25
也就是说,实际常量池项数量通常是:
constant_pool_count - 1
这是 Class 文件结构中一个比较特殊的地方。
2. 常量池中存放什么?
常量池中的常量大致可以分成两大类:
字面量
符号引用
2.1 字面量
字面量比较接近 Java 语言层面的常量概念。
例如:
String s = "hello";
int x = 10;
其中:
"hello"
10
都可以理解为字面量。
2.2 符号引用
符号引用是常量池中更重要的部分。
Java 代码在 javac 编译时,并不像 C/C++ 那样在编译阶段完成完整的链接过程。
Java 的链接过程主要发生在类加载阶段,也就是 JVM 加载 Class 文件之后。
因此,在 Class 文件中不会直接保存某个方法、字段在内存中的最终地址,而是使用符号引用来描述它们。
符号引用可以包括:
类或接口的全限定名
字段名称和描述符
方法名称和描述符
方法句柄
方法类型
动态调用点
例如下面这段代码:
public class Hello {
public static void main(String[] args) {
System.out.println("hello");
}
}
代码中会出现很多符号信息,例如:
Hello
main
java/lang/Object
java/lang/System
out
println
hello
([Ljava/lang/String;)V
(Ljava/lang/String;)V
这些信息通常都会进入常量池。
3.为什么字节码要依赖常量池?
对于下面这行代码:
System.out.println("hello");
编译成字节码后,可能类似:
getstatic #2
ldc #3
invokevirtual #4
这里的:
#2
#3
#4
就是常量池索引。
也就是说,字节码指令中并不会直接写完整的信息:
调用 java/io/PrintStream.println 方法
而是写成:
调用常量池中的第 4 项
这样做有两个好处:
第一,Class 文件结构更加紧凑。
第二,更适合 JVM 在运行时进行符号解析和动态链接。
可以简单理解为:
字节码指令负责“做什么”
常量池负责“具体是谁”
例如:
invokevirtual #4
表示:
执行一个虚方法调用,具体调用哪个方法,要去常量池第 4 项中查。
4.常量池中常见的常量类型
常量池中的每个元素都有自己的类型,常见类型如下:
| 常量池类型 | 含义 |
|---|---|
CONSTANT_Utf8 | UTF-8 编码的字符串,比如类名、方法名、字段名 |
CONSTANT_Class | 类或接口的符号引用 |
CONSTANT_String | 字符串常量 |
CONSTANT_Fieldref | 字段引用 |
CONSTANT_Methodref | 方法引用 |
CONSTANT_InterfaceMethodref | 接口方法引用 |
CONSTANT_NameAndType | 名称和描述符 |
CONSTANT_Integer | int 类型常量 |
CONSTANT_Long | long 类型常量 |
CONSTANT_Double | double 类型常量 |
CONSTANT_InvokeDynamic | 动态调用相关,常见于 Lambda 表达式 |
其中需要特别注意:
CONSTANT_Class、CONSTANT_Fieldref、CONSTANT_Methodref 等并不是直接保存目标对象本身,
而是保存对其他常量池项的引用。
也就是说,常量池内部也存在大量“索引指向索引”的结构。
七、类的基本信息
常量池之后,是类自身的一些描述信息,主要包括:
access_flags
this_class
super_class
interfaces
fields
methods
attributes
1.access_flags:类访问标志
access_flags 用来描述当前类或接口的访问属性。
例如:
public final class User {
}
这个类可能包含如下访问标志:
ACC_PUBLIC
ACC_FINAL
ACC_SUPER
常见访问标志如下:
| 标志 | 含义 |
|---|---|
ACC_PUBLIC | 是否为 public 类型 |
ACC_FINAL | 是否为 final 类型 |
ACC_SUPER | JVM 调用父类方法时使用的特殊语义 |
ACC_INTERFACE | 是否为接口 |
ACC_ABSTRACT | 是否为抽象类或接口 |
ACC_ANNOTATION | 是否为注解类型 |
ACC_ENUM | 是否为枚举类型 |
注意:
access_flags 是一个位标志集合。
也就是说,一个类可以同时拥有多个访问标志。
2.this_class:当前类
this_class 表示当前 Class 文件描述的是哪个类。
不过这里并不会直接保存类名字符串,而是保存一个指向常量池的索引。
例如:
this_class -> #7
#7 = Class #8
#8 = Utf8 Hello
它的含义是:
当前类是 Hello。
这里的查找过程是:
this_class
↓
CONSTANT_Class
↓
CONSTANT_Utf8
↓
真正的类名
3. super_class:父类
super_class 表示当前类的父类。
例如:
public class Hello {
}
虽然代码中没有显式继承任何类,但 Java 中的普通类默认继承:
java.lang.Object
所以它的父类信息通常是:
super_class -> java/lang/Object
需要注意的是,在 Class 文件中,类名通常使用 / 分隔包路径,而不是使用 .。
因此 Class 文件中会写成:
java/lang/Object
而不是:
java.lang.Object
4. interfaces:接口表
interfaces 用来描述当前类实现了哪些接口,或者当前接口继承了哪些父接口。
例如:
public class UserService implements Service, AutoCloseable {
}
那么接口表中会记录:
Service
AutoCloseable
不过和 this_class、super_class 一样,接口表中直接保存的也不是接口名字符串,而是常量池索引。
5. fields:字段表
fields 用于描述当前类或接口中声明的字段。
例如:
public class User {
private int age;
private String name;
}
这个类中声明了两个字段:
age
name
因此 Class 文件中的字段表会有两个字段信息。
字段表中的每个字段都使用 field_info 结构表示。
field_info {
u2 access_flags; // 字段访问标志
u2 name_index; // 字段名索引
u2 descriptor_index; // 字段描述符索引
u2 attributes_count; // 字段属性数量
attribute_info attributes[attributes_count];
}
可以把一个字段理解为由下面几部分组成:
字段访问权限
字段名
字段类型描述符
字段属性
5.1 字段描述符
字段描述符用于描述字段的数据类型。
常见类型描述符如下:
| Java 类型 | 描述符 |
|---|---|
int | I |
long | J |
float | F |
double | D |
boolean | Z |
char | C |
byte | B |
short | S |
String | Ljava/lang/String; |
int[] | [I |
String[] | [Ljava/lang/String; |
例如:
private String name;
对应字段描述符是:
Ljava/lang/String;
再例如:
private int[] scores;
对应字段描述符是:
[I
需要注意:
void 的描述符是 V,但 void 只能用于方法返回值,不能用于字段类型。
6. methods:方法表
methods 用来描述当前类或接口中声明的方法。
例如:
public class Hello {
public void sayHello() {
System.out.println("hello");
}
}
这个 Class 文件中至少会包含两个方法:
<init>
sayHello
为什么会有 <init>?
因为即使你没有手动编写构造方法,Java 编译器也会自动生成一个默认构造方法。
例如:
public class Hello {
}
编译器会自动补充类似下面的构造逻辑:
public Hello() {
super();
}
在 Class 文件中,实例构造方法的名字叫:
<init>
6.1 method_info 结构
方法表中的每个方法都使用 method_info 结构表示:
method_info {
u2 access_flags; // 方法访问标志
u2 name_index; // 方法名索引
u2 descriptor_index; // 方法描述符索引
u2 attributes_count; // 方法属性数量
attribute_info attributes[attributes_count];
}
可以把一个方法理解为由下面几部分组成:
方法访问权限
方法名
方法描述符
方法属性
不过要注意:
method_info 本身并不直接等于方法的执行逻辑。
方法真正的字节码指令通常存放在它的 Code 属性中。
6.2 方法描述符
方法描述符用于描述方法的:
参数类型
返回值类型
格式如下:
(参数类型列表)返回值类型
例如:
public void test()
对应方法描述符是:
()V
含义是:
无参数,返回 void。
再例如:
public int add(int a, int b)
对应方法描述符是:
(II)I
含义是:
两个 int 参数,返回 int。
再例如:
public String hello(String name, int age)
对应方法描述符是:
(Ljava/lang/String;I)Ljava/lang/String;
含义是:
参数是 String 和 int,返回 String。
7. attributes:属性表
属性表是 Class 文件中非常重要的扩展机制。
很多信息并不是直接放在 Class 文件主结构中的,而是通过属性表来保存。
常见属性包括:
| 属性 | 作用 |
|---|---|
Code | 方法的字节码指令 |
LineNumberTable | 字节码和源码行号的对应关系 |
LocalVariableTable | 局部变量信息 |
SourceFile | 源文件名 |
ConstantValue | 常量字段的值 |
Exceptions | 方法声明抛出的异常 |
InnerClasses | 内部类信息 |
Signature | 泛型签名 |
RuntimeVisibleAnnotations | 运行时可见注解 |
StackMapTable | 字节码验证使用的栈帧信息 |
属性表的设计非常灵活。
它使 JVM 可以在不破坏 Class 文件整体结构的前提下,不断扩展新的能力。
例如:
泛型
注解
Lambda
模块化
字节码验证
调试信息
很多能力都依赖属性表或常量池扩展来实现。
7.1 Code 属性
对于普通方法而言,真正的字节码指令通常存放在 Code 属性中。
例如:
public int add(int a, int b) {
return a + b;
}
这个方法本身的 method_info 只记录:
方法名:add
方法描述符:(II)I
访问标志:public
方法属性:Code
真正的执行逻辑在 Code 属性里。
Code 属性的大致结构如下:
Code_attribute {
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
exception_table exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
其中几个核心字段是:
| 字段 | 含义 |
|---|---|
max_stack | 操作数栈的最大深度 |
max_locals | 局部变量表所需的空间大小 |
code_length | 字节码指令长度 |
code | 真正的字节码指令 |
exception_table | 异常处理表 |
attributes | Code 属性内部的其他属性 |
7.1.1 max_stack
max_stack 表示当前方法执行过程中,操作数栈可能达到的最大深度。
JVM 执行字节码时采用的是基于操作数栈的执行模型。
例如:
int c = a + b;
对应的字节码可能类似:
iload_1
iload_2
iadd
istore_3
执行过程可以理解为:
iload_1 将局部变量 a 压入操作数栈
iload_2 将局部变量 b 压入操作数栈
iadd 弹出两个 int,相加后把结果压回操作数栈
istore_3 将栈顶结果保存到局部变量 c
所以 JVM 需要提前知道当前方法执行时操作数栈最多需要多深,这就是 max_stack 的作用。
7.1.2 max_locals
max_locals 表示当前方法所需的局部变量表大小。
局部变量表中通常保存:
this 引用
方法参数
方法内部定义的局部变量
例如:
public int add(int a, int b) {
int c = a + b;
return c;
}
对于实例方法来说,局部变量表可能是:
| slot | 内容 |
|---|---|
| 0 | this |
| 1 | a |
| 2 | b |
| 3 | c |
所以这个方法的 max_locals 至少需要 4 个 slot。
需要注意:
实例方法的局部变量表第 0 个 slot 通常是 this。
静态方法没有 this。
long 和 double 类型会占用两个 slot。
7.1.3 code
code 字段保存的就是真正的 JVM 字节码指令。
例如:
public int add(int a, int b) {
return a + b;
}
对应字节码可能是:
0: iload_1
1: iload_2
2: iadd
3: ireturn
含义是:
iload_1 加载第 1 个局部变量,也就是 a
iload_2 加载第 2 个局部变量,也就是 b
iadd 执行 int 加法
ireturn 返回 int 类型结果
八、使用 javap 观察 Class 文件
可以使用 javap -v 查看一个 Class 文件的详细结构。
源文件如下:
public class TestClassFile {
private int m = 10;
private static final String MESSAGE = "hello class file";
public TestClassFile() {
}
public int inc() {
return m++;
}
public int add(int a, int b) {
int c = a + b;
return c;
}
public static void main(String[] args) {
TestClassFile obj = new TestClassFile();
int result = obj.add(1, 2);
System.out.println(MESSAGE);
System.out.println(result);
}
}
其Class文件如下:
Classfile /mnt/data/classdemo/TestClassFile.class
Last modified Jun 8, 2026; size 953 bytes
SHA-256 checksum 62fc3507c5b639af60734c10c35d8fb982bad75751a1b6c812245daa1620f43a
Compiled from "TestClassFile.java"
public class TestClassFile
minor version: 0
major version: 65
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #8 // TestClassFile
super_class: #2 // java/lang/Object
interfaces: 0, fields: 2, methods: 4, 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 // TestClassFile.m:I
#8 = Class #10 // TestClassFile
#9 = NameAndType #11:#12 // m:I
#10 = Utf8 TestClassFile
#11 = Utf8 m
#12 = Utf8 I
#13 = Methodref #8.#3 // TestClassFile."<init>":()V
#14 = Methodref #8.#15 // TestClassFile.add:(II)I
#15 = NameAndType #16:#17 // add:(II)I
#16 = Utf8 add
#17 = Utf8 (II)I
#18 = Fieldref #19.#20 // java/lang/System.out:Ljava/io/PrintStream;
#19 = Class #21 // java/lang/System
#20 = NameAndType #22:#23 // out:Ljava/io/PrintStream;
#21 = Utf8 java/lang/System
#22 = Utf8 out
#23 = Utf8 Ljava/io/PrintStream;
#24 = String #25 // hello class file
#25 = Utf8 hello class file
#26 = Methodref #27.#28 // java/io/PrintStream.println:(Ljava/lang/String;)V
#27 = Class #29 // java/io/PrintStream
#28 = NameAndType #30:#31 // println:(Ljava/lang/String;)V
#29 = Utf8 java/io/PrintStream
#30 = Utf8 println
#31 = Utf8 (Ljava/lang/String;)V
#32 = Methodref #27.#33 // java/io/PrintStream.println:(I)V
#33 = NameAndType #30:#34 // println:(I)V
#34 = Utf8 (I)V
#35 = Utf8 MESSAGE
#36 = Utf8 Ljava/lang/String;
#37 = Utf8 ConstantValue
#38 = Utf8 Code
#39 = Utf8 LineNumberTable
#40 = Utf8 LocalVariableTable
#41 = Utf8 this
#42 = Utf8 LTestClassFile;
#43 = Utf8 inc
#44 = Utf8 ()I
#45 = Utf8 a
#46 = Utf8 b
#47 = Utf8 c
#48 = Utf8 main
#49 = Utf8 ([Ljava/lang/String;)V
#50 = Utf8 args
#51 = Utf8 [Ljava/lang/String;
#52 = Utf8 obj
#53 = Utf8 result
#54 = Utf8 SourceFile
#55 = Utf8 TestClassFile.java
{
private int m;
descriptor: I
flags: (0x0002) ACC_PRIVATE
private static final java.lang.String MESSAGE;
descriptor: Ljava/lang/String;
flags: (0x001a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL
ConstantValue: String hello class file
public TestClassFile();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 10
7: putfield #7 // Field m:I
10: return
LineNumberTable:
line 5: 0
line 2: 4
line 6: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this LTestClassFile;
public int inc();
descriptor: ()I
flags: (0x0001) ACC_PUBLIC
Code:
stack=4, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #7 // Field m:I
5: dup_x1
6: iconst_1
7: iadd
8: putfield #7 // Field m:I
11: ireturn
LineNumberTable:
line 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 12 0 this LTestClassFile;
public int add(int, int);
descriptor: (II)I
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=4, args_size=3
0: iload_1
1: iload_2
2: iadd
3: istore_3
4: iload_3
5: ireturn
LineNumberTable:
line 13: 0
line 14: 4
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this LTestClassFile;
0 6 1 a I
0 6 2 b I
4 2 3 c I
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: new #8 // class TestClassFile
3: dup
4: invokespecial #13 // Method "<init>":()V
7: astore_1
8: aload_1
9: iconst_1
10: iconst_2
11: invokevirtual #14 // Method add:(II)I
14: istore_2
15: getstatic #18 // Field java/lang/System.out:Ljava/io/PrintStream;
18: ldc #24 // String hello class file
20: invokevirtual #26 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
23: getstatic #18 // Field java/lang/System.out:Ljava/io/PrintStream;
26: iload_2
27: invokevirtual #32 // Method java/io/PrintStream.println:(I)V
30: return
LineNumberTable:
line 18: 0
line 19: 8
line 20: 15
line 21: 23
line 22: 30
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 args [Ljava/lang/String;
8 23 1 obj LTestClassFile;
15 16 2 result I
}
SourceFile: "TestClassFile.java"
下面来分块对应解析:
1.javap额外信息
Classfile /mnt/data/classdemo/TestClassFile.class
Last modified Jun 8, 2026; size 953 bytes
SHA-256 checksum 62fc3507c5b639af60734c10c35d8fb982bad75751a1b6c812245daa1620f43a
Compiled from "TestClassFile.java"
这部分是javap工具额外展示的文件信息。
2.魔数
因为是用javap查看的,因此不会直接展示魔数,如果使用十六进制工具查看那么就会看到0XCAFEBABE
3.版本号
minor version: 0
major version: 65
4.常量池
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
...
#55 = Utf8 TestClassFile.java
从上述可以看出,常量池就是整个Class文件的符号仓库:
类名
父类名
字段名
字段描述符
方法名
方法描述符
字符串常量
字段引用
方法引用
属性名
局部变量名
源文件名
4.1 示例:父类构造方法引用
#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
#1本身并没有直接保存完整字符串,而是引用了其他常量池项,其他常量池项又引用其他其他常量池项。
把它串起来:
#1 Methodref
├── #2 Class
│ └── #4 Utf8 java/lang/Object
└── #3 NameAndType
├── #5 Utf8 <init>
└── #6 Utf8 ()V
最终含义就是:
调用 java/lang/Object 的 <init>()V 方法
也就是 Java 代码中的:
super();
4.2 示例:字段引用m
#7 = Fieldref #8.#9 // TestClassFile.m:I
它表示字段引用:
TestClassFile.m:I
也就是:
private int m;
继续展开:
#8 = Class #10 // TestClassFile
#9 = NameAndType #11:#12 // m:I
#10 = Utf8 TestClassFile
#11 = Utf8 m
#12 = Utf8 I
结构是:
#7 Fieldref
├── #8 Class
│ └── #10 Utf8 TestClassFile
└── #9 NameAndType
├── #11 Utf8 m
└── #12 Utf8 I
其中:
m = 字段名
I = int 类型描述符
所以:
TestClassFile.m:I
意思是:
TestClassFile 类中的 int 类型字段 m
4.3 示例:add方法引用
#14 = Methodref #8.#15 // TestClassFile.add:(II)I
它表示方法引用:
TestClassFile.add:(II)I
继续展开:
#8 = Class #10 // TestClassFile
#15 = NameAndType #16:#17 // add:(II)I
#16 = Utf8 add
#17 = Utf8 (II)I
结构是:
#14 Methodref
├── #8 Class
│ └── #10 Utf8 TestClassFile
└── #15 NameAndType
├── #16 Utf8 add
└── #17 Utf8 (II)I
其中:
(II)I
表示:
参数是两个 int,返回值是 int
对应 Java 方法:
public int add(int a, int b)
4.4 示例:System.out.println
main 方法里有:
System.out.println(MESSAGE);
System.out.println(result);
所以常量池里有这些内容:
#18 = Fieldref #19.#20 // java/lang/System.out:Ljava/io/PrintStream;
#19 = Class #21 // java/lang/System
#20 = NameAndType #22:#23 // out:Ljava/io/PrintStream;
#21 = Utf8 java/lang/System
#22 = Utf8 out
#23 = Utf8 Ljava/io/PrintStream;
这表示字段:
System.out
也就是:
java/lang/System.out:Ljava/io/PrintStream;
然后是两个 println 方法:
#26 = Methodref #27.#28 // java/io/PrintStream.println:(Ljava/lang/String;)V
#32 = Methodref #27.#33 // java/io/PrintStream.println:(I)V
它们分别对应:
println(String)
println(int)
代码里打印了两个不同类型的值:
System.out.println(MESSAGE); // String
System.out.println(result); // int
所以对应两个方法描述符:
(Ljava/lang/String;)V
(I)V
5.access_flags:类访问标志
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
Class 文件结构中是:
u2 access_flags;
这里表示当前类的访问标志。
0x0021 = ACC_PUBLIC + ACC_SUPER
含义是:
ACC_PUBLIC:这个类是 public 的
ACC_SUPER:使用新的 invokespecial 语义调用父类方法
对应类定义:
public class TestClassFile {
}
因为类是 public,所以有:
ACC_PUBLIC
普通现代 Java 类通常也会带:
ACC_SUPER
6. this_class:当前类
this_class: #8 // TestClassFile
Class 文件结构中是:
u2 this_class;
它表示当前 Class 文件描述的是哪个类。 这里:
this_class = #8
然后看常量池:
#8 = Class #10 // TestClassFile
#10 = Utf8 TestClassFile
所以:
this_class: #8
最终表示:
当前类是 TestClassFile
注意,这里并不是直接保存字符串 TestClassFile,而是保存常量池索引。
查找链路是:
this_class
↓
#8 Class
↓
#10 Utf8 TestClassFile
7.super_class:父类
super_class: #2 // java/lang/Object
Class 文件结构中是:
u2 super_class;
这里:
super_class = #2
看常量池:
#2 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
所以当前类的父类是:
java/lang/Object
虽然源码里没有写:
extends Object
但是 Java 中所有普通类默认继承:
java.lang.Object
所以 Class 文件中会记录父类为:
java/lang/Object
8.interfaces_count和interfaces:接口表
interfaces: 0
Class 文件结构中是:
u2 interfaces_count;
u2 interfaces[interfaces_count];
这里表示:
interfaces_count = 0
也就是说:
public class TestClassFile {
}
这个类没有实现任何接口。如果类是这样:
public class TestClassFile implements Runnable {
}
那么这里就不会是 0,而是会记录 Runnable 对应的常量池索引。
9.fileds_count和fields:字段表
field: 2
表示这个类有 2 个字段。根据之前的源码,这两个字段是:
private int m = 10;
private static final String MESSAGE = "hello class file";
也就是:
字段 1:m
字段 2:MESSAGE
9.1 字段m
private int m = 10;
它的字段信息大致是:
private int m;
descriptor: I
flags: (0x0002) ACC_PRIVATE
其中:
m = 字段名
I = int 类型描述符
它在常量池中对应:
#11 = Utf8 m
#12 = Utf8 I
#7 = Fieldref #8.#9 // TestClassFile.m:I
注意:
private int m = 10;
这个 10 并不是字段表里直接存的初始值。
它会被编译到构造方法 <init> 中:
4: aload_0
5: bipush 10
7: putfield #7 // Field m:I
也就是说,对实例字段 m 的初始化是在构造方法中完成的。
9.2 字段MESSAGE
private static final String MESSAGE = "hello class file";
它会对应:
private static final java.lang.String MESSAGE;
descriptor: Ljava/lang/String;
flags: (0x001a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL
ConstantValue: String hello class file
它在常量池里能看到相关信息:
#24 = String #25 // hello class file
#25 = Utf8 hello class file
#35 = Utf8 MESSAGE
#36 = Utf8 Ljava/lang/String;
#37 = Utf8 ConstantValue
这里要注意:
private static final String MESSAGE = "hello class file";
因为它是编译期常量,所以值会记录在字段的 ConstantValue 属性中。
并且在 main 方法中使用它时,字节码是:
18: ldc #24 // String hello class file
也就是说,main 方法并没有通过 getstatic 去读取 MESSAGE 字段,而是直接把字符串常量 "hello class file" 加载出来了。
10. methods_count和methods: 方法表
methods: 4
表示当前类有 4 个方法,这 4 个方法是:
1. TestClassFile()
2. inc()
3. add(int, int)
4. main(String[])
在 Class 文件中,构造方法不叫 TestClassFile,而叫:
<init>
所以实际上是:
<init>
inc
add
main
每个方法的结构都是:
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
也就是:
方法访问标志
方法名
方法描述符
方法属性
方法真正的字节码在:
Code 属性
10.1 方法1:构造方法
public TestClassFile();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 10
7: putfield #7 // Field m:I
10: return
源码中构造方法是:
public TestClassFile() {
}
但因为有实例字段初始化:
private int m = 10;
所以编译器会把它合并进构造方法中,等价于:
public TestClassFile() {
super();
this.m = 10;
}
10.2 方法2:inc()
public TestClassFile();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 10
7: putfield #7 // Field m:I
10: return
源码中构造方法是:
public TestClassFile() {
}
但因为有实例字段初始化:
private int m = 10;
所以编译器会把它合并进构造方法中,等价于:
public TestClassFile() {
super();
this.m = 10;
}
10.3 方法3:add(int,int)
public int add(int, int);
descriptor: (II)I
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=4, args_size=3
0: iload_1
1: iload_2
2: iadd
3: istore_3
4: iload_3
5: ireturn
源码是:
public int add(int a, int b) {
int c = a + b;
return c;
}
10.4 方法4:main(String[] )
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: new #8 // class TestClassFile
3: dup
4: invokespecial #13 // Method "<init>":()V
7: astore_1
8: aload_1
9: iconst_1
10: iconst_2
11: invokevirtual #14 // Method add:(II)I
14: istore_2
15: getstatic #18 // Field java/lang/System.out:Ljava/io/PrintStream;
18: ldc #24 // String hello class file
20: invokevirtual #26 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
23: getstatic #18 // Field java/lang/System.out:Ljava/io/PrintStream;
26: iload_2
27: invokevirtual #32 // Method java/io/PrintStream.println:(I)V
30: return
源码是:
public static void main(String[] args) {
TestClassFile obj = new TestClassFile();
int result = obj.add(1, 2);
System.out.println(MESSAGE);
System.out.println(result);
}