JVM - Class文件结构
# 1. 引言:JVM 的基石 - Class 文件
# 1.1 Java 的跨平台性基石
Java 语言的核心优势之一就是其跨平台性 (Write Once, Run Anywhere)。开发者编写的 Java 源代码 (.java
文件) 被 Java 编译器编译成字节码 (Bytecode) 文件 (.class
文件)。这种字节码是一种平台无关的中间表示,可以在任何安装了兼容 Java 虚拟机 (JVM) 的平台上运行,无需重新编译。
虽然现代许多脚本语言(如 Python, Ruby 等)通过强大的解释器也实现了跨平台,但 Java 通过编译成统一的字节码格式,并在 JVM 这个虚拟层面上执行,奠定了其早期广泛流行的基础。
# 1.2 JVM:跨语言的平台
更进一步地,Java 虚拟机本身的设计目标并不局限于 Java 语言。JVM 的核心是与一种特定格式的二进制文件——Class 文件——相关联。任何编程语言,只要其编译器能够将源代码转换为符合 JVM 规范的 Class 文件,那么该语言编写的程序就可以在 JVM 上运行。
因此,健壮、统一且功能强大的 Class 文件结构,成为了连接各种编程语言与 Java 虚拟机平台的基石和桥梁。Scala, Groovy, Kotlin, Clojure 等众多运行在 JVM 上的语言,都证明了 JVM 作为跨语言平台的成功。
要让 Java 程序在 JVM 上正确运行,Java 源代码必须通过前端编译器编译成符合 JVM 规范的 Class 文件。
# 1.3 Java 前端编译器简介
- 前端编译器: 主要负责将符合特定语言语法规范的源代码转换为符合虚拟机规范的中间表示(对 Java 而言就是 Class 文件)。它主要进行词法分析、语法分析、语义分析以及生成字节码等步骤。常见的 Java 前端编译器包括:
- javac: Oracle JDK 和 OpenJDK 中标准的前端编译器。进行全量式编译。
- ECJ (Eclipse Compiler for Java): Eclipse IDE 内置的编译器,支持增量式编译(只编译修改过的部分),编译效率通常更高。Tomcat 也使用 ECJ 编译 JSP 文件。
- 后端编译器 (JIT): JVM 内部的即时编译器 (Just-In-Time Compiler),如 HotSpot 的 C1、C2 编译器。它负责在运行时将热点字节码编译成本地机器码,以提高执行效率。前端编译器通常不进行深入的编译优化,将优化任务交给后端的 JIT 编译器。
# 1.4 透过字节码看代码细节
理解 Class 文件和其中的字节码指令,有助于深入理解 Java 代码的底层执行机制,解释一些看似奇怪的现象,并进行更底层的性能分析。
面试题示例:
- Class 文件结构包含哪些部分?
- 字节码指令有哪些?
Integer x = 5; int y = 5;
比较x == y
为true
的过程涉及哪些字节码?
代码示例 1: Integer 缓存
// 文件名: IntegerTest.java
public class IntegerTest {
public static void main(String[] args) {
// 自动装箱,调用 Integer.valueOf(10)
// 10 在 Integer 缓存 (-128 到 127) 范围内,返回缓存中的同一个对象
Integer i1 = 10;
Integer i2 = 10;
System.out.println(i1 == i2); // 输出 true,因为 i1 和 i2 指向缓存中的同一个对象
// 自动装箱,调用 Integer.valueOf(128)
// 128 超出缓存范围,每次都会 new 一个新的 Integer 对象
Integer i3 = 128;
Integer i4 = 128;
System.out.println(i3 == i4); // 输出 false,因为 i3 和 i4 指向不同的堆对象
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
对应字节码片段 (使用 javap -v IntegerTest.class
查看):
// ... (省略部分)
0: bipush 10 // 将 10 推送至操作数栈顶 (byte push)
2: invokestatic #2 // 调用静态方法 Integer.valueOf(I)Ljava/lang/Integer;
5: astore_1 // 将栈顶引用存入局部变量表 slot 1 (i1)
6: bipush 10 // 再次推送 10
8: invokestatic #2 // 再次调用 Integer.valueOf(I)Ljava/lang/Integer;
11: astore_2 // 将栈顶引用存入局部变量表 slot 2 (i2)
// ... (省略比较和打印部分)
28: sipush 128 // 将 128 推送至操作数栈顶 (short push)
31: invokestatic #2 // 调用静态方法 Integer.valueOf(I)Ljava/lang/Integer;
34: astore_3 // 将栈顶引用存入局部变量表 slot 3 (i3)
35: sipush 128 // 再次推送 128
38: invokestatic #2 // 再次调用 Integer.valueOf(I)Ljava/lang/Integer;
41: astore 4 // 将栈顶引用存入局部变量表 slot 4 (i4)
// ... (省略比较和打印部分)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
分析: 字节码层面都调用了 Integer.valueOf()
。true
和 false
的区别源于 Integer.valueOf()
的实现逻辑(内部缓存)。
代码示例 2: String 拼接
// 文件名: StringTest.java
public class StringTest {
public static void main(String[] args) {
// "+" 拼接会通过 StringBuilder 实现
// new String("hello") 和 new String("world") 在堆上创建了两个不同的对象
// StringBuilder.append().toString() 最终也返回一个新的 String 对象 (在堆上)
String str = new String("hello") + new String("world");
// "helloworld" 是一个字符串字面量,会被放入字符串常量池 (String Pool)
String str1 = "helloworld";
// str 指向堆上的新对象,str1 指向常量池中的对象,地址不同
System.out.println(str == str1); // 输出 false
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
对应字节码片段:
// ... (省略 StringBuilder 初始化)
7: new #4 // 创建一个新的 String 对象 (对应 new String("hello"))
10: dup
11: ldc #5 // 从常量池加载 "hello"
13: invokespecial #6 // 调用 String 的构造器 <init>(Ljava/lang/String;)V
16: invokevirtual #7 // 调用 StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: new #4 // 创建一个新的 String 对象 (对应 new String("world"))
22: dup
23: ldc #8 // 从常量池加载 "world"
25: invokespecial #6 // 调用 String 的构造器 <init>(Ljava/lang/String;)V
28: invokevirtual #7 // 调用 StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
31: invokevirtual #9 // 调用 StringBuilder.toString()Ljava/lang/String; -> 返回堆上的新 String 对象
34: astore_1 // 将引用存入 slot 1 (str)
35: ldc #10 // 从常量池加载 "helloworld" (这个是字面量,指向常量池对象)
37: astore_2 // 将引用存入 slot 2 (str1)
// ... (省略比较和打印部分)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
分析: 字节码清晰地显示了 str
是通过 StringBuilder
构建并在堆上创建的新对象,而 str1
是直接从常量池加载的。因此 str == str1
为 false
。
代码示例 3: 类初始化与实例初始化过程
// 文件名: SonTest.java
/**
* 演示类加载、初始化以及实例化的顺序,特别是父类与子类、成员变量赋值过程。
* 成员变量(非静态的)赋值过程:
* 1. 默认初始化 (零值)
* 2. 显式初始化 / 代码块中初始化 (按代码顺序)
* 3. 构造器中初始化
* 4. 对象创建后,通过 对象.变量 或 对象.方法 进行赋值
*
* 实例初始化 (<init>) 过程:
* 1. super() (隐式或显式调用父类构造器)
* 2. 成员变量的显式初始化和初始化块
* 3. 构造器内的代码
*/
public class SonTest {
public static void main(String[] args) {
// 1. 加载 Father 类和 Son 类
// 2. 初始化 Father 类 (<clinit>) - (本例无静态内容,省略)
// 3. 初始化 Son 类 (<clinit>) - (本例无静态内容,省略)
// 4. 创建 Son 对象实例
// a. 调用 Son 的构造器
// b. Son 构造器首先隐式调用 Father 构造器 super()
// i. Father 构造器开始执行
// ii. 调用 this.print() -> 此时 this 是 Son 对象,发生动态绑定,调用 Son 的 print()
// - Son 的 print() 访问 Son 实例的 x -> 此时 Son 的 x 处于默认初始化阶段,为 0
// - 输出 "Son.x = 0"
// iii. 执行 Father 构造器中的 x = 20 -> 修改 Father 实例变量的 x 为 20
// iv. Father 构造器执行完毕
// c. 继续执行 Son 构造器
// i. Son 的实例变量 x 进行显式初始化 -> Son 的 x = 30
// ii. 调用 this.print() -> 调用 Son 的 print()
// - Son 的 print() 访问 Son 实例的 x -> 此时 Son 的 x 为 30
// - 输出 "Son.x = 30"
// iii. 执行 Son 构造器中的 x = 40 -> 修改 Son 实例变量的 x 为 40
// iv. Son 构造器执行完毕
// 5. 将 Son 对象赋值给 Father 类型的引用 f (向上转型)
Father f = new Son();
// 6. 访问 f.x
// - 变量访问看左边 (静态类型),f 的静态类型是 Father
// - 访问的是 Father 类中定义的实例变量 x
// - 在步骤 4.b.iii 中,Father 的 x 被赋值为 20
System.out.println(f.x); // 输出 20
}
}
class Father {
// 成员变量显式初始化 (在构造器 super() 调用之后,构造器代码执行之前)
int x = 10; // (实际上在子类构造器中,这个初始化会被子类的覆盖,但父类构造器执行时还未覆盖)
public Father() {
// 调用 print() 方法,由于 this 是 Son 对象,会调用 Son 的 print()
this.print();
// 在构造器中赋值
this.x = 20; // 修改的是 Father 实例变量 x 的值
}
public void print() {
System.out.println("Father.x = " + x);
}
}
class Son extends Father {
// 成员变量显式初始化
int x = 30; // 这个 x 是 Son 类自己的实例变量,与 Father 的 x 不同
public Son() {
// 隐式调用 super() 在这里发生
// 调用 print() 方法
this.print();
// 在构造器中赋值
this.x = 40; // 修改的是 Son 实例变量 x 的值
}
@Override // 重写 print 方法
public void print() {
// 访问的是 Son 类自己的实例变量 x
System.out.println("Son.x = " + x);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
输出结果:
Son.x = 0
Son.x = 30
20
2
3
分析: 这个例子揭示了:
- 实例初始化方法
<init>
的执行顺序(先父后子)。 - 方法调用的动态绑定(
this.print()
实际调用的是子类的方法)。 - 成员变量的访问取决于引用的静态类型(
f.x
访问的是Father
的x
)。 - 子类实例化时,父类和子类的成员变量初始化时机和相互影响。
理解这些需要结合 JVM 的类加载、初始化和方法调用机制,而字节码是理解这些机制的基础。
# 2. Class 文件:虚拟机的二进制基石
# 2.1 Class 文件是什么?
Java 源代码 (.java
) 经过编译器(如 javac
)编译后,生成的就是字节码文件 (.class
)。这是一种二进制文件,其内部包含的是 JVM 指令集和符号表等信息。它与 C/C++ 编译后直接生成特定平台的机器码不同,字节码是平台无关的,需要由 JVM 解释执行或即时编译 (JIT) 成机器码后执行。
# 2.2 字节码指令 (Bytecode Instruction)
JVM 指令由一个单字节的操作码 (opcode) 和零到多个操作数 (operand) 构成。操作码标识了要执行的操作(如 aload_0
, invokevirtual
),操作数则提供了该操作所需的数据(如常量池索引、局部变量索引等)。许多指令只有一个操作码,没有操作数。
(aload_0 是操作码,无操作数;bipush 是操作码,10 是操作数)
# 2.3 如何解读 Class 文件?
由于 Class 文件是二进制格式,直接用文本编辑器打开是乱码。解读它的方式有:
- 十六进制编辑器: 使用如 Notepad++ (需 HEX-Editor 插件)、Binary Viewer、UltraEdit 等工具,以十六进制形式查看字节流。这需要完全对照 JVM 规范来手动解析,非常繁琐。
javap
命令: JDK 自带的反汇编工具。javap -v <ClassName>.class
可以输出可读性较好的 Class 文件结构信息,包括常量池、字段、方法、字节码指令等。这是最常用的官方工具。- IDE 插件: 如 IntelliJ IDEA 的
jclasslib Bytecode Viewer
或 Eclipse 的 Bytecode Outline 插件,提供图形化界面展示 Class 文件结构,更为直观。
# 3. Class 文件结构详解
# 3.1 Class 文件格式概述
根据 《Java 虚拟机规范 (Java SE 8 版)》,Class 文件是一组以 8 位字节为基础单位的二进制流。它没有像 XML 那样的分隔符,各项数据(无论是字节顺序还是数量)都严格按照规定排列,不允许改变。
Class 文件格式采用类似 C 语言结构体的方式存储数据,只包含两种数据类型:
- 无符号数 (Unsigned Number): 基本数据类型,用于表示数字、索引、数量或 UTF-8 编码的字符串。以
u1
,u2
,u4
,u8
分别代表 1、2、4、8 个字节的无符号数。 - 表 (Table): 由多个无符号数或其他表构成的复合数据类型。表没有固定长度,通常其前面会有表示其大小的计数器。所有表都习惯以
_info
结尾 (如constant_pool_info
,field_info
)。整个 Class 文件本质上就是一张大表。
示例代码 (Demo.java
):
// 文件名: Demo.java
public class Demo {
private int num = 1; // 实例变量
// 实例方法
public int add() {
num = num + 2;
return num;
}
}
2
3
4
5
6
7
8
9
10
对应的 Class 文件字节码 (部分十六进制):
理解了 Class 文件的结构,理论上就可以从字节码反编译回 Java 源代码(反编译工具如 jad
, jd-gui
就是这样做的)。
# 3.2 Class 文件整体结构
一个 Class 文件的结构(按顺序)包含以下组成部分:
组成部分 | 类型 | 名称 | 数量 | 长度 (字节) | 说明 |
---|---|---|---|---|---|
魔数 | u4 | magic | 1 | 4 | Class 文件标识 (0xCAFEBABE) |
版本号 | u2 | minor_version | 1 | 2 | 副版本号 |
u2 | major_version | 1 | 2 | 主版本号 | |
常量池 | u2 | constant_pool_count | 1 | 2 | 常量池大小计数器 (从 1 开始) |
cp_info | constant_pool | constant_pool_count-1 | 可变 | 常量池表项集合 | |
访问标志 | u2 | access_flags | 1 | 2 | 类或接口的访问修饰符 |
类索引 | u2 | this_class | 1 | 2 | 指向常量池中本类的 CONSTANT_Class_info |
父类索引 | u2 | super_class | 1 | 2 | 指向常量池中父类的 CONSTANT_Class_info |
接口索引集合 | u2 | interfaces_count | 1 | 2 | 实现的接口数量 |
u2 | interfaces | interfaces_count | 2 * count | 指向常量池中接口的 CONSTANT_Class_info 数组 | |
字段表集合 | u2 | fields_count | 1 | 2 | 字段数量 |
field_info | fields | fields_count | 可变 | 字段信息表集合 | |
方法表集合 | u2 | methods_count | 1 | 2 | 方法数量 |
method_info | methods | methods_count | 可变 | 方法信息表集合 | |
属性表集合 | u2 | attributes_count | 1 | 2 | 类级别属性数量 |
attribute_info | attributes | attributes_count | 可变 | 类级别属性信息表集合 |
注: cp_info
, field_info
, method_info
, attribute_info
都是表的类型。
接下来详细解析每个部分。
# 4. 魔数 (Magic Number)
- 位置: Class 文件开头的 4 个字节。
- 类型:
u4
(4 字节无符号数)。 - 固定值:
0xCAFEBABE
("咖啡宝贝")。 - 作用: 唯一作用是标识这个文件是一个有效的、能被 JVM 接受的 Class 文件。JVM 在加载 Class 文件时首先检查这 4 个字节。如果不是
0xCAFEBABE
,会直接抛出java.lang.ClassFormatError
。 - 安全性: 使用魔数而非文件扩展名 (
.class
) 来识别文件类型,可以防止通过简单修改扩展名来伪装文件类型,提高了安全性。
(图中前 4 字节 CA FE BA BE)
# 5. Class 文件版本号 (Version)
- 位置: 紧跟魔数之后的 4 个字节。
- 结构:
- 第 5、6 字节:
minor_version
(副版本号, u2)。 - 第 7、8 字节:
major_version
(主版本号, u2)。
- 第 5、6 字节:
- 作用: 标识编译该 Class 文件的 JDK 版本。JVM 通过版本号来判断是否能够兼容执行该 Class 文件。
(图中 00 00 是 minor_version, 00 34 是 major_version)
主版本号与 JDK 版本对应关系:
JDK 版本 | 主版本号 (十进制) | 主版本号 (十六进制) |
---|---|---|
1.1 | 45 | 0x2D |
1.2 | 46 | 0x2E |
1.3 | 47 | 0x2F |
1.4 | 48 | 0x30 |
1.5 (5) | 49 | 0x31 |
1.6 (6) | 50 | 0x32 |
1.7 (7) | 51 | 0x33 |
1.8 (8) | 52 | 0x34 |
9 | 53 | 0x35 |
10 | 54 | 0x36 |
11 | 55 | 0x37 |
... | ... | ... |
规律: JDK 1.1 对应主版本 45,之后每个大版本加 1。副版本号在早期使用较多,现在通常为 0。
兼容性规则:
- 向下兼容: 高版本的 JVM 可以执行由低版本编译器生成的 Class 文件。
- 向上不兼容: 低版本的 JVM 不能执行由高版本编译器生成的 Class 文件。尝试加载时会抛出
java.lang.UnsupportedClassVersionError
。 - 注意: 开发环境 JDK 版本与生产环境 JDK 版本的不一致是导致此错误的常见原因。
# 6. 常量池集合 (Constant Pool)
常量池是 Class 文件中最重要、内容最丰富的部分,是整个 Class 文件的资源仓库。它存储了编译期生成的各种字面量 (Literals) 和符号引用 (Symbolic References),这些信息在类加载后会被载入方法区的运行时常量池。
# 6.1 常量池计数器 (constant_pool_count
)
- 位置: 版本号之后。
- 类型:
u2
(2 字节无符号数)。 - 作用: 表示常量池中常量项的数量。
- 特殊计数方式: 这个计数值从 1 开始。例如,如果
constant_pool_count
的值为 22,那么常量池中实际有 21 个常量项,索引范围是 1 到 21。 - 索引 0 的保留: 常量池索引从 1 开始,索引 0 被保留。用于表示某些情况下“不引用任何常量池项”的含义。
(图中 00 16 = 22,表示有 21 个常量项)
# 6.2 常量池表 (constant_pool[]
)
- 位置: 紧跟计数器之后。
- 结构: 由
constant_pool_count - 1
个常量表项 (cp_info
) 组成。 - 内容: 主要存储两大类信息:
- 字面量 (Literal):
- 文本字符串 (如
"Hello"
) - 基本数据类型的值 (如
int 100
,float 3.14f
) - 声明为
final
的常量值 (编译期常量)
- 文本字符串 (如
- 符号引用 (Symbolic Reference):
- 类和接口的全限定名 (Fully Qualified Name, e.g.,
java/lang/Object
) - 字段的名称和描述符 (Name and Descriptor, e.g.,
num I
) - 方法的名称和描述符 (Name and Descriptor, e.g.,
add ()I
)
- 类和接口的全限定名 (Fully Qualified Name, e.g.,
- 字面量 (Literal):
# 6.3 字面量 vs. 符号引用
- 字面量: 接近于 Java 源代码中常量的概念。
- 符号引用:
- 目的: 在编译阶段,编译器无法知道所引用的类、字段、方法的具体内存地址。因此,使用符号来描述它们。
- 组成: 通常由全限定名、简单名称和描述符组成。
- 与内存布局无关: 符号引用不依赖于 JVM 的具体内存实现。
- 动态链接: JVM 在类加载或首次使用时,会进行动态链接,将常量池中的符号引用解析 (Resolve) 为直接引用 (Direct Reference)(如内存地址指针、偏移量或句柄)。只有解析后的直接引用才能被 JVM 直接使用。
# 6.4 名称与描述符
全限定名 (Fully Qualified Name): 将类或接口的包名中的
.
替换为/
,例如java.lang.String
的全限定名是java/lang/String
。在 Class 文件中,为了分隔,末尾有时会加;
。简单名称 (Simple Name): 没有类型或参数修饰的字段或方法名,如
num
,add
。描述符 (Descriptor): 用于精确描述字段类型、方法参数列表和返回值类型的字符串。
基本类型:
字符 类型 B byte C char D double F float I int J long S short Z boolean V void 对象类型:
L
+ 类的全限定名 +;
。例如Ljava/lang/String;
。数组类型: 每个维度前加一个
[
。例如:int[]
->[I
String[][]
->[[Ljava/lang/String;
double[][][]
->[[[D
方法描述符:
(
+ 参数描述符序列 +)
+ 返回值描述符。String toString()
->()Ljava/lang/String;
int add(int x, int y)
->(II)I
void main(String[] args)
->([Ljava/lang/String;)V
Object check(int i, double d, Thread t)
->(IDLjava/lang/Thread;)Ljava/lang/Object;
示例:数组打印输出中的描述符
// 文件名: ArrayTest.java
package cn.kbt;
import java.util.Date;
public class ArrayTest {
public static void main(String[] args) {
Object[] arr = new Object[10];
// 输出: [Ljava.lang.Object;@<hashcode>
// [ 表示一维数组, Ljava/lang/Object; 是 Object 的描述符
System.out.println(arr);
String[] arr1 = new String[10];
// 输出: [Ljava/lang.String;@<hashcode>
System.out.println(arr1);
long[][] arr2 = new long[10][];
// 输出: [[J@<hashcode>
// [[ 表示二维数组, J 是 long 的描述符
System.out.println(arr2);
Date[] arr3 = new Date[10];
// 输出: [Ljava/util/Date;@<hashcode> (注意 Date 需要导入)
System.out.println(arr3);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 6.5 常量池项的类型与结构 (cp_info
)
常量池中的每个表项 (cp_info
) 都有一个统一的结构:第一个字节是 tag
(标志位),表示该常量项的类型。JVM 根据 tag
值来确定如何解析后续的字节。
Java SE 8 定义了 14 种常量类型:
Tag (值) | 常量类型 | 描述 |
---|---|---|
1 | CONSTANT_Utf8_info | UTF-8 编码的字符串 (用于名称、描述符等) |
3 | CONSTANT_Integer_info | int 型字面量 |
4 | CONSTANT_Float_info | float 型字面量 |
5 | CONSTANT_Long_info | long 型字面量 (占两个槽位) |
6 | CONSTANT_Double_info | double 型字面量 (占两个槽位) |
7 | CONSTANT_Class_info | 类或接口的符号引用 (指向 Utf8 全限定名) |
8 | CONSTANT_String_info | String 类型字面量 (指向 Utf8 字符串值) |
9 | CONSTANT_Fieldref_info | 字段的符号引用 (指向 Class 和 NameAndType) |
10 | CONSTANT_Methodref_info | 类中方法的符号引用 (指向 Class 和 NameAndType) |
11 | CONSTANT_InterfaceMethodref_info | 接口中方法的符号引用 (指向 Class 和 NameAndType) |
12 | CONSTANT_NameAndType_info | 字段或方法的名称和描述符 (指向 Utf8 名称和 Utf8 描述符) |
15 | CONSTANT_MethodHandle_info | 方法句柄 (JDK 7+) |
16 | CONSTANT_MethodType_info | 方法类型 (JDK 7+) |
18 | CONSTANT_InvokeDynamic_info | 动态方法调用点 (JDK 7+) |
常量项结构特点:
- 每种类型的常量项都有其特定的结构(包含哪些字段,每个字段占多少字节)。
- 除了
CONSTANT_Utf8_info
(其长度由内部字段决定),其他 13 种常量项的长度是固定的。 CONSTANT_Long_info
和CONSTANT_Double_info
占用 8 字节,它们在常量池中会占据两个连续的索引位 (entry)。如果一个 long 或 double 常量在索引n
,则下一个可用的索引是n+2
。
示例:常量池解析 (对照字节码和 javap
输出)
- (需要结合具体 Class 文件的字节码和
javap -v
的输出来手动或借助工具进行详细解析,演示常量项的 tag、内容和相互引用关系。这部分内容在静态文本中较难完美呈现,建议参考视频或使用 jclasslib 等工具实践。)
(这些图例帮助定位和理解常量池在字节码中的位置和部分内容)
# 7. 访问标志 (Access Flags)
- 位置: 常量池之后。
- 类型:
u2
(2 字节无符号数)。 - 作用: 用于识别类 (Class) 或接口 (Interface) 层次的访问信息和属性,如可见性 (public)、是否 final、是否接口、是否抽象、是否注解、是否枚举等。
- 表示方式: 使用一个位掩码 (Bitmask)。每个标志对应一个特定的位,通过按位或 (
|
) 操作组合多个标志。
类/接口访问标志 (Java SE 8):
标志名称 | 值 (Hex) | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | public ,类或接口可从包外访问。 |
ACC_FINAL | 0x0010 | final ,类不允许被继承。 (不能与 ACC_ABSTRACT 同时设置) |
ACC_SUPER | 0x0020 | (历史遗留,默认都设置) 确定 invokespecial 指令语义。 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口。 (必须同时设置 ACC_ABSTRACT ) |
ACC_ABSTRACT | 0x0400 | abstract ,类或接口为抽象。 (不能与 ACC_FINAL 同时设置) |
ACC_SYNTHETIC | 0x1000 | 由编译器生成,非源代码编写。 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解类型。 (必须同时设置 ACC_INTERFACE ) |
ACC_ENUM | 0x4000 | 标识这是一个枚举类型。 (类标志) |
示例解析:
(图中值为 0x0021)
0x0021 = 0x0001 | 0x0020 = ACC_PUBLIC | ACC_SUPER
。这表示该 Class 文件定义的是一个 public
类,并且设置了 ACC_SUPER
标志。
使用 jclasslib 查看:
约束:
ACC_INTERFACE
为真时,ACC_ABSTRACT
必须为真,且不能有ACC_FINAL
,ACC_SUPER
,ACC_ENUM
。ACC_INTERFACE
为假时(即是类),不能同时设置ACC_FINAL
和ACC_ABSTRACT
。ACC_ANNOTATION
为真时,ACC_INTERFACE
必须为真。
# 8. 类索引、父类索引、接口索引集合 (Indices)
这三项紧跟在访问标志之后,用于确定该类的继承和实现关系。它们的值都是指向常量池的索引。
this_class
(类索引):- 类型:
u2
。 - 作用: 指向常量池中一个
CONSTANT_Class_info
项,该项包含了当前类的全限定名。
- 类型:
super_class
(父类索引):- 类型:
u2
。 - 作用: 指向常量池中一个
CONSTANT_Class_info
项,该项包含了当前类的直接父类的全限定名。 - 特殊情况:
- 如果父类是
java.lang.Object
,则该索引指向代表Object
的CONSTANT_Class_info
。 - 对于
java.lang.Object
类本身,其super_class
索引为 0 (表示没有父类)。 - Java 是单继承,所以只有一个父类索引。
- 父类不能是
final
的。
- 如果父类是
- 类型:
interfaces
(接口索引集合):- 结构: 由两部分组成:
interfaces_count
(u2): 表示当前类(或接口)直接实现(或继承) 的接口数量。interfaces[]
(u2 数组): 一个长度为interfaces_count
的数组,每个元素都是指向常量池中一个CONSTANT_Class_info
项的索引,该项代表一个已实现的接口。接口的顺序与源代码中implements
(或extends
for interface) 子句中的顺序一致。
- 结构: 由两部分组成:
(图中依次是
this_class
指向 #4, super_class
指向 #5, interfaces_count
为 0)
这些索引在类加载的链接阶段(特别是解析)非常关键,用于确定类的继承层次结构。
# 9. 字段表集合 (Fields)
字段表 (fields
) 用于描述类或接口中声明的所有字段 (变量)。
- 范围: 只包含当前类或接口声明的字段,不包括从父类或父接口继承来的字段。
- 内容: 包括类变量 (static fields) 和实例变量 (instance fields),不包括方法内部的局部变量。
- 可能包含编译器生成的字段: 例如,内部类为了访问外部类实例,编译器可能会自动添加一个指向外部类实例的隐藏字段。
# 9.1 字段计数器 (fields_count
)
- 类型:
u2
。 - 作用: 表示当前类或接口包含的
field_info
表项的数量。如果为 0,表示没有声明任何字段。
# 9.2 字段表 (fields[]
)
- 结构: 由
fields_count
个field_info
结构组成。 field_info
结构: 每个field_info
代表一个字段的完整描述信息。
field_info
结构:
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
u2 | access_flags | 1 | 字段的访问标志 (public, private, static, final, volatile, transient等) |
u2 | name_index | 1 | 指向常量池中字段简单名称 (Utf8) 的索引 |
u2 | descriptor_index | 1 | 指向常量池中字段描述符 (Utf8) 的索引 (如 "I", "Ljava/lang/String;") |
u2 | attributes_count | 1 | 该字段的属性数量 |
attribute_info | attributes | attributes_count | 字段的属性表集合 (如 ConstantValue ) |
(图中
fields_count
为 0x0001,表示有一个字段)
# 9.3 字段访问标志 (access_flags
)
与类的访问标志类似,字段也有自己的访问标志位掩码:
标志名称 | 值 (Hex) | 含义 |
---|---|---|
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 | 标识为枚举常量 |
(图中值为 0x0002,表示
ACC_PRIVATE
)
# 9.4 字段名索引 (name_index
) 和 描述符索引 (descriptor_index
)
这两个 u2
类型的索引分别指向常量池中的 CONSTANT_Utf8_info
项,用于确定字段的简单名称和类型描述符。
# 9.5 属性表集合 (attributes
)
字段可以有关联的属性,提供额外信息。
ConstantValue
属性: 如果一个字段是静态常量 (static final
),并且其类型是基本类型或 String,编译器会为其生成ConstantValue
属性,其值指向常量池中该字段的字面量值。JVM 在类初始化时会使用这个属性来初始化静态字段。// ConstantValue 属性结构 ConstantValue_attribute { u2 attribute_name_index; // 指向常量池 "ConstantValue" u4 attribute_length; // 恒为 2 u2 constantvalue_index; // 指向常量池中该字段的实际常量值 (如 Integer, Long, String 等) }
1
2
3
4
5
6其他属性: 如
Deprecated
,Signature
(用于泛型),RuntimeVisibleAnnotations
等。
字段重名: Java 语言不允许字段重名。但在字节码层面,只要两个字段的描述符不同,即使简单名称相同,也是合法的。
# 10. 方法表集合 (Methods)
方法表 (methods
) 用于描述类或接口中声明的所有方法。
- 范围: 只包含当前类或接口声明的方法(包括构造器、静态方法、实例方法、类初始化方法
<clinit>
、实例初始化方法<init>
),不包括从父类或父接口继承来的方法。 - 可能包含编译器生成的方法:
- 实例初始化方法
<init>
: 对应构造器。即使源码没写构造器,编译器也会生成一个默认的<init>
。 - 类初始化方法
<clinit>
: 由静态变量赋值语句和静态代码块合并产生。只有当类中有静态变量赋值或静态代码块时才会生成。
- 实例初始化方法
# 10.1 方法计数器 (methods_count
)
- 类型:
u2
。 - 作用: 表示当前类或接口包含的
method_info
表项的数量。
# 10.2 方法表 (methods[]
)
- 结构: 由
methods_count
个method_info
结构组成。 method_info
结构: 每个method_info
代表一个方法的完整描述信息。
method_info
结构 (与 field_info
类似):
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
u2 | access_flags | 1 | 方法的访问标志 (public, private, static, final, synchronized, native, abstract 等) |
u2 | name_index | 1 | 指向常量池中方法简单名称 (Utf8, 如 "add", " |
u2 | descriptor_index | 1 | 指向常量池中方法描述符 (Utf8, 如 "()I", "(II)V") 的索引 |
u2 | attributes_count | 1 | 该方法的属性数量 |
attribute_info | attributes | attributes_count | 方法的属性表集合 (如 Code , Exceptions ) |
# 10.3 方法访问标志 (access_flags
)
方法的访问标志:
标志名称 | 值 (Hex) | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | public |
ACC_PRIVATE | 0x0002 | private |
ACC_PROTECTED | 0x0004 | protected |
ACC_STATIC | 0x0008 | static |
ACC_FINAL | 0x0010 | final (方法不允许被覆盖) |
ACC_SYNCHRONIZED | 0x0020 | synchronized (方法调用需获取 monitor lock) |
ACC_BRIDGE | 0x0040 | 由编译器生成的桥接方法 (用于泛型和协变返回类型) |
ACC_VARARGS | 0x0080 | 方法接受可变数量参数 |
ACC_NATIVE | 0x0100 | native (方法由本地语言实现, 无 Code 属性) |
ACC_ABSTRACT | 0x0400 | abstract (方法无实现, 无 Code 属性) |
ACC_STRICT | 0x0800 | strictfp (浮点计算使用严格精度) |
ACC_SYNTHETIC | 0x1000 | 由编译器生成 |
(图中
<init>
方法 (构造器) 的标志为 0x0001 = ACC_PUBLIC
)
约束:
ACC_ABSTRACT
方法不能是final
,native
,private
,static
,synchronized
,strictfp
。- 接口中的方法默认是
public
,abstract
(JDK 8 前)。
# 10.4 方法名索引 (name_index
) 和 描述符索引 (descriptor_index
)
与字段类似,指向常量池中的 Utf8 项,确定方法的简单名称和描述符(参数列表+返回值)。
# 10.5 属性表集合 (attributes
)
方法可以有多种属性,最重要的通常是 Code
属性。
Code
属性: 只有非native
、非abstract
的方法才会有Code
属性。它包含了方法的字节码指令、操作数栈最大深度、局部变量表大小、异常处理表以及可能的嵌套属性(如LineNumberTable
,LocalVariableTable
)。(详细结构见下一节属性表部分)。Exceptions
属性: 列出了方法声明中throws
子句显式抛出的受检异常 (Checked Exceptions)。- 其他属性:
Deprecated
,Signature
(泛型签名),RuntimeVisibleAnnotations
等。
方法重载与字节码:
- Java 语言: 重载 (Overload) 要求方法名相同,但参数列表(类型、数量、顺序)必须不同。返回值不能作为区分重载的依据。
- Class 文件格式: 只要描述符 (
descriptor_index
) 不同(即参数列表或返回值任一不同),即使方法名 (name_index
) 相同,也可以合法共存于同一个 Class 文件中。这意味着字节码层面允许仅返回值不同的“重载”,但这在 Java 语言层面是不允许的。
# 11. 属性表集合 (Attributes)
属性表 (attributes
) 用于携带 Class 文件、字段表、方法表中未包含的附加信息。它是 Class 文件格式中最灵活的部分。
- 位置: 可以出现在 Class 文件末尾(类级别属性)、字段表 (
field_info
) 中、方法表 (method_info
) 中,甚至Code
属性内部。 - 作用: 提供诸如源代码文件名、行号映射、局部变量信息、常量值、异常表、泛型签名、注解信息、调试信息等。
- 扩展性: JVM 规范预定义了多种属性,但也允许编译器厂商或开发者自定义属性。JVM 运行时会忽略它不认识的属性。
# 11.1 属性通用格式 (attribute_info
)
所有属性都遵循一个通用的结构:
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
u2 | attribute_name_index | 1 | 指向常量池中表示属性名称 (Utf8) 的索引 |
u4 | attribute_length | 1 | 表示属性信息 (info 部分) 的字节长度 |
u1 | info | attribute_length | 属性的具体内容,其结构由 attribute_name_index 决定 |
# 11.2 属性计数器 (attributes_count
)
在 Class 文件、field_info
、method_info
、Code_attribute
中都有 attributes_count
字段 (u2),表示其后跟随的 attribute_info
表项的数量。
# 11.3 常见的预定义属性详解
1. Code
属性 (位于 method_info
中)
- 核心: 存储 Java 方法编译后的字节码指令。
- 存在条件: 仅存在于非
abstract
、非native
的方法中。 - 结构:
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
u2 | attribute_name_index | 1 | 指向 "Code" |
u4 | attribute_length | 1 | 整个 Code 属性后续内容的长度 |
u2 | max_stack | 1 | 操作数栈 (Operand Stack) 的最大深度 |
u2 | max_locals | 1 | 局部变量表 (Local Variable Table) 所需的存储空间 (Slot 数量) |
u4 | code_length | 1 | 字节码指令部分的长度 (字节数) |
u1[] | code | code_length | 实际的字节码指令序列 |
u2 | exception_table_length | 1 | 异常处理表的条目数 |
exception_info [] | exception_table | exception_length | 异常处理表 (定义 try-catch 逻辑) |
u2 | attributes_count | 1 | Code 属性内部嵌套的属性数量 |
attribute_info [] | attributes | attributes_count | 嵌套的属性表 (如 LineNumberTable , LocalVariableTable ) |
exception_info
结构 (异常表条目):
类型 | 名称 | 含义 |
---|---|---|
u2 | start_pc | try 块的起始字节码偏移量 (包含) |
u2 | end_pc | try 块的结束字节码偏移量 (不包含) |
u2 | handler_pc | catch 块处理逻辑的起始字节码偏移量 |
u2 | catch_type | 指向常量池 CONSTANT_Class_info 的索引,表示捕获的异常类型。如果为 0,表示捕获所有异常 (finally ) |
2. ConstantValue
属性 (位于 field_info
中)
- 见 9.5 节。用于
static final
常量字段的初始化。
3. Deprecated
属性 (位于 ClassFile, field_info
, method_info
中)
- 作用: 标记类、字段或方法已被弃用。编译器或 IDE 会据此给出警告。
- 结构: 仅包含
attribute_name_index
和attribute_length
(恒为 0),没有info
部分。
4. Exceptions
属性 (位于 method_info
中)
作用: 列出方法声明的
throws
子句中显式声明的受检异常 (Checked Exceptions)。不包括RuntimeException
或Error
。结构:
Exceptions_attribute { u2 attribute_name_index; // 指向 "Exceptions" u4 attribute_length; u2 number_of_exceptions; // 异常数量 u2 exception_index_table[number_of_exceptions]; // 指向常量池 CONSTANT_Class_info 的索引数组 }
1
2
3
4
5
6
5. InnerClasses
属性 (位于 ClassFile 中)
- 作用: 记录类中定义的内部类(包括成员内部类、局部内部类、匿名内部类)以及该类是哪个类的内部类的信息。
- 存在条件: 如果一个类的常量池包含指向非包成员(即内部类)的
CONSTANT_Class_info
,则必须包含此属性。
6. LineNumberTable
属性 (位于 Code
属性中)
作用: 建立字节码指令偏移量 (PC) 与源代码行号之间的映射关系。主要用于调试 (在异常堆栈中显示行号) 和代码覆盖率分析。
可选属性: 即使没有这个属性,代码也能正常运行,只是调试信息会缺失。
javac -g:none
可以不生成此属性。结构:
LineNumberTable_attribute { u2 attribute_name_index; // 指向 "LineNumberTable" u4 attribute_length; u2 line_number_table_length; // 行号表条目数 { u2 start_pc; // 字节码偏移量 u2 line_number; // 对应的源代码行号 } line_number_table[line_number_table_length]; }
1
2
3
4
5
6
7
8
9line_number_table
按start_pc
递增排序。
7. LocalVariableTable
属性 (位于 Code
属性中)
作用: 描述方法执行过程中局部变量的信息,包括变量名、类型描述符、在字节码中的作用范围 (生命周期)、在局部变量表中的 Slot 索引。主要用于调试 (查看变量值)。
可选属性: 缺失不影响运行,但调试器无法显示参数或局部变量名。
javac -g:vars
或javac -g
(默认包含 vars) 会生成此属性。结构:
LocalVariableTable_attribute { u2 attribute_name_index; // 指向 "LocalVariableTable" u4 attribute_length; u2 local_variable_table_length; // 局部变量条目数 { u2 start_pc; // 作用域起始字节码偏移量 u2 length; // 作用域长度 u2 name_index; // 指向常量池变量名 (Utf8) 的索引 u2 descriptor_index; // 指向常量池变量类型描述符 (Utf8) 的索引 u2 index; // 该变量在局部变量表中使用的 Slot 索引 } local_variable_table[local_variable_table_length]; }
1
2
3
4
5
6
7
8
9
10
11
12start_pc
到start_pc + length
(不含) 是该变量的作用域。index
指明了变量在栈帧局部变量表中的位置。Slot 可以被复用。
8. Signature
属性 (位于 ClassFile, field_info
, method_info
中)
- 作用: 为了支持 Java 5 引入的泛型而添加。记录类、接口、方法或字段的泛型签名信息 (包含类型变量或参数化类型)。普通的描述符只能表示原始类型 (Raw Type)。
- 存在条件: 当类、方法或字段的签名包含泛型信息时,编译器会生成此属性。
- 用途: 使反射 API (如
getGenericSuperclass
,getGenericParameterTypes
) 能够获取到泛型类型信息。
9. SourceFile
属性 (位于 ClassFile 中)
作用: 记录生成此 Class 文件的源文件名。
结构:
SourceFile_attribute { u2 attribute_name_index; // 指向 "SourceFile" u4 attribute_length; // 恒为 2 u2 sourcefile_index; // 指向常量池源文件名 (Utf8) 的索引 }
1
2
3
4
5
10. 其他属性:
SourceDebugExtension
: 存储额外的调试信息。StackMapTable
(JDK 6+): 用于新的类型检查验证器,加速类加载验证。BootstrapMethods
(JDK 7+): 用于支持invokedynamic
指令。- 注解相关属性 (
RuntimeVisibleAnnotations
,RuntimeInvisibleAnnotations
,RuntimeVisibleParameterAnnotations
,RuntimeInvisibleParameterAnnotations
,AnnotationDefault
): 用于存储注解信息。
# 12. 使用 javap
指令解析 Class 文件
手动解析二进制 Class 文件非常繁琐且容易出错。JDK 提供了 javap
工具,可以方便地反解析 Class 文件,输出可读的结构信息。
# 12.1 javap
的作用
- 根据
.class
文件反解析出类的内部结构,如:- Class 文件基本信息(版本号、访问标志等)
- 常量池内容
- 字段信息
- 方法信息(包括字节码指令、操作数栈和局部变量表大小)
- 行号表 (
LineNumberTable
) - 局部变量表 (
LocalVariableTable
) - 异常表 (
exception_table
) - 类和方法的属性(如
Signature
,ConstantValue
等)
- 有助于理解 Java 代码的底层实现、JVM 的执行过程,以及进行 Debug 和性能分析。
# 12.2 javac -g
参数
javap
能显示的信息多少,取决于编译时 javac
使用的参数。
javac YourClass.java
: 默认只生成运行时必需的信息。javap
可能无法显示局部变量名、行号等调试信息。javac -g YourClass.java
: 生成所有调试信息,包括局部变量表、行号表等。javac -g:none YourClass.java
: 不生成任何调试信息。javac -g:{lines,vars,source} YourClass.java
: 分别生成行号、局部变量、源文件信息。-g
等同于-g:lines,vars,source
。
注意: 主流 IDE (如 IntelliJ IDEA, Eclipse) 默认编译时会加上 -g
或类似选项,以方便调试。
# 12.3 javap
常用选项
用法: javap <options> <classes>
常用选项:
- (无选项): 输出类的
public
字段和方法。 -p
或-private
: 输出所有类和成员 (包括private
)。-l
: 输出行号表和局部变量表。-c
: 对方法进行反汇编,显示字节码指令。-s
: 输出内部类型签名 (显示描述符)。-verbose
或-v
: 输出最详细的信息,包括常量池、访问标志、栈大小、参数数量等(包含-l
,-c
,-s
的信息)。 最常用。-constants
: 只显示static final
常量。-sysinfo
: 显示系统信息 (路径, 修改时间, MD5)。
推荐组合: javap -v -p <ClassName>
(显示所有详细信息,包括私有成员)。
# 12.4 javap
解析示例 (JavapTest.java
)
// 文件名: JavapTest.java
public class JavapTest {
private int num;
boolean flag;
protected char gender;
public String info;
public static final int COUNTS = 1; // 常量
static { // 静态代码块
String url = "www.youngkbt.com";
}
{ // 实例代码块
info = "java";
}
public JavapTest() { // 公有构造器
}
private JavapTest(boolean flag) { // 私有构造器
this.flag = flag;
}
private void methodPrivate() { // 私有方法
}
int getNum(int i) { // 包访问权限方法
return num + i;
}
protected char showGender() { // 受保护方法
return gender;
}
public void showInfo() { // 公有方法
int i = 10; // 局部变量
System.out.println(info + i);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
执行反解析:
javac -g JavapTest.java # 编译,确保包含调试信息
javap -v -p JavapTest.class > JavapTest.txt # 反解析并输出到文件
2
JavapTest.txt
内容及注释 (节选重要部分):
Classfile /path/to/JavapTest.class // 文件路径
Last modified 2024-03-28; size XXXX bytes // 修改时间, 大小
MD5 checksum XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX // MD5
Compiled from "JavapTest.java" // 源文件名
public class JavapTest // 类声明, public
minor version: 0 // 副版本
major version: 52 // 主版本 (JDK 8)
flags: (0x0021) ACC_PUBLIC, ACC_SUPER // 访问标志
Constant pool: // 常量池 (内容非常多, #n 代表索引)
#1 = Methodref #16.#46 // java/lang/Object."<init>":()V (父类构造器引用)
#2 = String #47 // "java" (字面量)
#3 = Fieldref #15.#48 // this.info:Ljava/lang/String; (字段引用)
#4 = Fieldref #15.#49 // this.flag:Z
#5 = Fieldref #15.#50 // this.num:I
#6 = Fieldref #15.#51 // this.gender:C
#7 = Fieldref #52.#53 // System.out:Ljava/io/PrintStream; (静态字段引用)
#8 = Class #54 // java/lang/StringBuilder (类引用)
#9 = Methodref #8.#46 // java/lang/StringBuilder."<init>":()V (方法引用)
#10 = Methodref #8.#55 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#11 = Methodref #8.#56 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
#12 = Methodref #8.#57 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#13 = Methodref #58.#59 // java/io/PrintStream.println:(Ljava/lang/String;)V
#14 = String #60 // "www.youngkbt.com" (字面量)
#15 = Class #61 // JavapTest (本类引用)
#16 = Class #62 // java/lang/Object (父类引用)
#17 = Utf8 num // 字段/方法名
#18 = Utf8 I // int 描述符
#19 = Utf8 flag
#20 = Utf8 Z // boolean 描述符
#21 = Utf8 gender
#22 = Utf8 C // char 描述符
#23 = Utf8 info
#24 = Utf8 Ljava/lang/String; // String 描述符
#25 = Utf8 COUNTS
#26 = Utf8 ConstantValue // 属性名
#27 = Integer 1 // int 字面量
#28 = Utf8 <init> // 构造器名
#29 = Utf8 ()V // void 返回值, 无参数描述符
#30 = Utf8 Code // 属性名
#31 = Utf8 LineNumberTable // 属性名
#32 = Utf8 LocalVariableTable // 属性名
#33 = Utf8 this // 变量名
#34 = Utf8 LJavapTest; // 本类描述符
#35 = Utf8 (Z)V // void 返回值, boolean 参数描述符
#36 = Utf8 methodPrivate
#37 = Utf8 getNum
#38 = Utf8 (I)I // int 返回值, int 参数描述符
#39 = Utf8 i // 变量名
#40 = Utf8 showGender
#41 = Utf8 ()C // char 返回值, 无参数描述符
#42 = Utf8 showInfo
#43 = Utf8 <clinit> // 类初始化方法名
#44 = Utf8 SourceFile // 属性名
#45 = Utf8 JavapTest.java // 源文件名
#46 = NameAndType #28:#29 // "<init>":()V (名称和类型)
// ... (更多 NameAndType, Utf8, Class 等项)
{ // 字段表 (fields)
private int num; // 字段声明
descriptor: I // 类型描述符
flags: (0x0002) ACC_PRIVATE // 访问标志
boolean flag;
descriptor: Z
flags: (0x0000) // 包访问权限 (无 public/private/protected)
protected char gender;
descriptor: C
flags: (0x0004) ACC_PROTECTED
public java.lang.String info;
descriptor: Ljava/lang/String;
flags: (0x0001) ACC_PUBLIC
public static final int COUNTS; // 静态常量字段
descriptor: I
flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 1 // ConstantValue 属性, 指向常量池 #27 (值为 1)
} // 字段表结束
{ // 方法表 (methods)
public JavapTest(); // 公有构造器 <init>
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code: // Code 属性
stack=2, locals=1, args_size=1 // 操作数栈深度2, 局部变量表大小1(只有this), 参数个数1(this)
0: aload_0 // 加载 this 到操作数栈
1: invokespecial #1 // 调用父类 Object 的 <init> 方法
4: aload_0 // 加载 this
5: ldc #2 // 加载常量池 "java"
7: putfield #3 // 将 "java" 赋值给 this.info 字段 (实例代码块内容在此执行)
10: return // 返回
LineNumberTable: // 行号表 (字节码偏移量 : 源代码行号)
line 20: 0 // <init> 方法体起始 (隐式 super())
line 18: 4 // 实例代码块内容 (info = "java")
line 22: 10 // 构造器结束
LocalVariableTable: // 局部变量表
Start Length Slot Name Signature // 起始PC, 长度, Slot索引, 变量名, 签名(泛型时用)
0 11 0 this LJavapTest; // this 变量的作用域和类型
private JavapTest(boolean); // 私有构造器 <init>
descriptor: (Z)V
flags: (0x0002) ACC_PRIVATE
Code:
stack=2, locals=2, args_size=2 // locals=2(this, flag), args_size=2(this, flag)
0: aload_0
1: invokespecial #1 // 调用 Object.<init>
4: aload_0
5: ldc #2 // 加载 "java"
7: putfield #3 // this.info = "java" (实例代码块)
10: aload_0 // 加载 this
11: iload_1 // 加载参数 flag (在局部变量表 slot 1)
12: putfield #4 // this.flag = flag (构造器代码)
15: return
LineNumberTable:
line 23: 0
line 18: 4
line 24: 10
line 25: 15
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 this LJavapTest;
0 16 1 flag Z // 参数 flag
private void methodPrivate(); // 私有方法
descriptor: ()V
flags: (0x0002) ACC_PRIVATE
Code:
stack=0, locals=1, args_size=1 // 无操作数栈需求, 局部变量只有 this
0: return
LineNumberTable:
line 28: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this LJavapTest;
int getNum(int); // 包访问权限方法
descriptor: (I)I
flags: (0x0000)
Code:
stack=2, locals=2, args_size=2 // stack=2 (num和i相加), locals=2(this, i), args=2(this, i)
0: aload_0 // 加载 this
1: getfield #5 // 获取 this.num 的值压栈
4: iload_1 // 加载参数 i (在 slot 1) 压栈
5: iadd // 将栈顶两个 int 相加, 结果压栈
6: ireturn // 返回栈顶 int 值
LineNumberTable:
line 30: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this LJavapTest;
0 7 1 i I // 参数 i
protected char showGender(); // 受保护方法
descriptor: ()C
flags: (0x0004) ACC_PROTECTED
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #6 // 获取 this.gender 的值压栈
4: ireturn // 返回栈顶 char 值 (实际当 int 返回)
LineNumberTable:
line 33: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LJavapTest;
public void showInfo(); // 公有方法
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=3, locals=2, args_size=1 // stack=3 (用于 StringBuilder 操作), locals=2(this, i), args=1(this)
0: bipush 10 // 将 10 压栈
2: istore_1 // 将栈顶 int 存入局部变量 slot 1 (i)
3: getstatic #7 // 获取 System.out 静态字段引用压栈
6: new #8 // 创建 StringBuilder 对象, 引用压栈
9: dup // 复制栈顶 StringBuilder 引用
10: invokespecial #9 // 调用 StringBuilder.<init>
13: aload_0 // 加载 this
14: getfield #3 // 获取 this.info 引用压栈
17: invokevirtual #10 // 调用 StringBuilder.append(String)
20: iload_1 // 加载局部变量 i (10) 压栈
21: invokevirtual #11 // 调用 StringBuilder.append(int)
24: invokevirtual #12 // 调用 StringBuilder.toString()
27: invokevirtual #13 // 调用 System.out.println(String)
30: return
LineNumberTable:
line 36: 0 // int i = 10;
line 37: 3 // System.out.println(...)
line 38: 30 // 方法结束
LocalVariableTable:
Start Length Slot Name Signature
0 31 0 this LJavapTest;
3 28 1 i I // 局部变量 i 的作用域从 PC=3 开始
static {}; // 静态初始化方法 <clinit>
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=1, locals=1, args_size=0 // locals=1 (用于 url 变量, 虽然字节码优化掉了), args=0
0: ldc #14 // 加载常量池 "www.youngkbt.com"
2: astore_0 // 将引用存入局部变量 slot 0 (虽然源码是 url, 字节码层面可能是临时变量)
3: return // 静态块执行完毕
LineNumberTable:
line 15: 0 // String url = ...
line 16: 3 // 静态块结束
LocalVariableTable: // 如果编译优化,这里可能没有 url 的条目
Start Length Slot Name Signature
// (根据编译器和优化级别,url 变量可能不会出现在局部变量表中)
} // 方法表结束
SourceFile: "JavapTest.java" // 源文件属性, 指向常量池 #45
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
分析 javap
输出的要点:
- 常量池是核心: 几乎所有信息(类名、父类名、接口名、字段名、方法名、类型描述符、字符串字面量、属性名等)都存储在常量池中,其他部分通过索引引用常量池。
- 访问标志: 清晰地展示了类、字段、方法的修饰符。
- 字段表: 列出了所有声明的字段及其类型和访问权限。
ConstantValue
属性揭示了编译期常量的初始值。 - 方法表:
- 包含所有声明的方法,包括编译器生成的
<init>
和<clinit>
。 Code
属性是理解方法执行的关键:stack
,locals
,args_size
提供了栈帧大小信息。- 字节码指令 (
aload_0
,invokespecial
,getfield
,putfield
,ldc
,new
,invokevirtual
,invokestatic
,ireturn
,istore_1
,iload_1
,iadd
等)是 JVM 执行的基本单元。 LineNumberTable
将字节码与源码关联起来。LocalVariableTable
显示了局部变量(包括this
和方法参数)的作用域和在栈帧中的存储位置 (Slot)。
- 包含所有声明的方法,包括编译器生成的
# 12.5 总结
javap
是一个强大的工具,可以帮助开发者:
- 理解 Java 代码在底层是如何执行的。
- 学习 JVM 指令集。
- 分析编译器行为(如代码块的合并、自动装箱拆箱、常量折叠等)。
- 辅助进行性能分析和 Debug。
- 验证 Class 文件的正确性。
# 13. Class 文件结构总结
- 基石: Class 文件是 JVM 执行的基础,是连接各种语言与 JVM 平台的桥梁。
- 格式: 严格定义的二进制流,由无符号数和表组成。
- 核心组成: 魔数、版本号、常量池、访问标志、类/父类/接口索引、字段表、方法表、属性表。
- 常量池: 存储字面量和符号引用,是 Class 文件信息的核心仓库。
- 字段表/方法表: 描述类中声明的字段和方法信息,包含访问标志、名称、描述符和属性。
- 属性表: 提供附加信息,如字节码 (
Code
)、调试信息 (LineNumberTable
,LocalVariableTable
)、泛型 (Signature
)、注解等。 javap
工具: 解析和理解 Class 文件内容的重要辅助手段。
深入理解 Class 文件结构,对于理解 JVM 的类加载机制、内存模型、字节码执行引擎以及进行高级性能调优都至关重要。