Java字节码学习总结-字节码如何加载、解析和执行
- 前言
- Class文件中的字节码如何被JVM加载、解析和执行
-
- 一、学习目标
- [二、Class 文件如何被 JVM 加载?](#二、Class 文件如何被 JVM 加载?)
-
- [2.1 Class 文件的基本结构](#2.1 Class 文件的基本结构)
- [2.2 类加载器如何获取 Class 文件?](#2.2 类加载器如何获取 Class 文件?)
- [2.3 HotSpot 类加载源码分析](#2.3 HotSpot 类加载源码分析)
- [三、Class 文件如何被 JVM 解析?](#三、Class 文件如何被 JVM 解析?)
-
- [3.1 ClassFileParser 的核心职责](#3.1 ClassFileParser 的核心职责)
- [3.2 常量池解析源码示意](#3.2 常量池解析源码示意)
- [3.3 字节码解析与方法 Code 属性](#3.3 字节码解析与方法 Code 属性)
- [四、JVM 如何验证、准备和解析 Class 文件?](#四、JVM 如何验证、准备和解析 Class 文件?)
-
- [4.1 验证:Verification](#4.1 验证:Verification)
- [4.2 准备:Preparation](#4.2 准备:Preparation)
- [4.3 解析:Resolution](#4.3 解析:Resolution)
- [五、JVM 解释器如何执行字节码?](#五、JVM 解释器如何执行字节码?)
-
- [5.1 Template Interpreter 的工作原理](#5.1 Template Interpreter 的工作原理)
- [5.2 TemplateTable 源码分析](#5.2 TemplateTable 源码分析)
- [5.3 TemplateInterpreter 源码分析](#5.3 TemplateInterpreter 源码分析)
- [5.4 方法执行帧如何工作?](#5.4 方法执行帧如何工作?)
- [六、JVM 如何通过 JIT 将字节码编译为机器码?](#六、JVM 如何通过 JIT 将字节码编译为机器码?)
-
- [6.1 JIT 的基本工作流程](#6.1 JIT 的基本工作流程)
- [6.2 CompileBroker 源码分析](#6.2 CompileBroker 源码分析)
- [6.3 C1 和 C2 编译器的区别](#6.3 C1 和 C2 编译器的区别)
- [6.4 JIT 如何优化字节码?](#6.4 JIT 如何优化字节码?)
- [七、字节码最终如何被 CPU 执行?](#七、字节码最终如何被 CPU 执行?)
-
- [7.1 字节码与机器码的区别](#7.1 字节码与机器码的区别)
- [7.2 解释器和 JIT 两条执行路径](#7.2 解释器和 JIT 两条执行路径)
- [7.3 不同 CPU 架构的执行差异](#7.3 不同 CPU 架构的执行差异)
- [八、通过 JVM 参数观察加载、解析和执行过程](#八、通过 JVM 参数观察加载、解析和执行过程)
-
- [8.1 查看 Class 文件结构](#8.1 查看 Class 文件结构)
- [8.2 观察类加载过程](#8.2 观察类加载过程)
- [8.3 观察解释器和 JIT 编译](#8.3 观察解释器和 JIT 编译)
- [8.4 观察 JIT 生成的机器码](#8.4 观察 JIT 生成的机器码)
- [九、OpenJDK 源码阅读路线](#九、OpenJDK 源码阅读路线)
- 十、建议形成的学习成果
- 十一、最终学习总结:系统工程师必须掌握的核心结论
前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。
Class文件中的字节码如何被JVM加载、解析和执行
一、学习目标
本次学习围绕 JVM 执行引擎展开,重点理解 Class 文件从进入 JVM 到最终被 CPU 执行的完整过程。
从系统工程师角度,不能仅仅理解 ClassLoader 如何加载 .class 文件,还需要深入 HotSpot 源码,理解:
-
Class 文件如何被类加载器读取并转换为 JVM 内部数据结构。
-
JVM 如何解析常量池、字段、方法和 Code 属性。
-
JVM 如何验证字节码的合法性和类型安全。
-
JVM 如何将符号引用解析为运行时可以使用的引用。
-
解释器如何通过模板分派机制执行字节码。
-
JIT 如何将热点字节码编译为本地机器码。
-
CPU 最终如何执行 JVM 生成的本地机器指令。
本次学习的核心是建立以下认知:

核心结论:Class 文件中的字节码是 JVM 的指令,不是 CPU 的原生指令。JVM 通过解释执行或即时编译,将字节码落实为当前硬件平台可以执行的机器指令。
二、Class 文件如何被 JVM 加载?
2.1 Class 文件的基本结构
Java 源代码经过 javac 编译后生成 .class 文件。
例如:
public class BytecodeDemo {
public static int add(int a, int b) {
return a + b;
}
public static void main(String[] args) {
int result = add(10, 20);
System.out.println(result);
}
}
编译:
javac BytecodeDemo.java
生成:
BytecodeDemo.class
Class 文件不是普通文本文件,而是一种按照 JVM 规范组织的二进制文件。
主要结构如下:
| 组成部分 | 职责 |
|---|---|
| Magic | Class 文件标识,固定为 0xCAFEBABE |
| Minor Version | 次版本号 |
| Major Version | 主版本号 |
| Constant Pool | 保存常量和符号引用 |
| Access Flags | 类的访问标志 |
| This Class | 当前类信息 |
| Super Class | 父类信息 |
| Interfaces | 接口信息 |
| Fields | 字段定义 |
| Methods | 方法定义 |
| Attributes | 类级别及其他属性 |
其中,Methods 中的方法可能包含 Code 属性。
Code 属性保存:
-
max_stack:操作数栈最大深度。 -
max_locals:局部变量表所需槽位数量。 -
code_length:字节码长度。 -
code[]:字节码指令序列。 -
exception_table:异常处理表。 -
attributes:方法相关的附加属性。
学习重点:Class 文件保存的是 JVM 可以识别的程序结构和指令,而不是 Java 源代码的简单复制。
2.2 类加载器如何获取 Class 文件?
Java 提供了类加载器体系:
| 类加载器 | 职责 |
|---|---|
| Bootstrap ClassLoader | 加载核心运行时类 |
| Platform ClassLoader | 加载平台类库 |
| Application ClassLoader | 加载应用程序类 |
通过 Java 代码观察:
public class ClassLoaderDemo {
public static void main(String[] args) {
// 获取当前类的 Class 对象
Class<?> clazz = BytecodeDemo.class;
// 获取定义当前类的类加载器
ClassLoader loader = clazz.getClassLoader();
// 输出类加载器
System.out.println(loader);
// 输出父类加载器
System.out.println(loader.getParent());
}
}
需要理解两个概念:
-
ClassLoader负责获取类的二进制数据。 -
JVM 负责将类数据解析为运行时类结构。
类的身份由类加载器和类的二进制名称共同确定。因此,两个不同类加载器加载的同名类,可以是两个完全不同的 JVM 类型。
2.3 HotSpot 类加载源码分析
重点源码:
src/hotspot/share/classfile/
classLoader.cpp
classLoader.hpp
systemDictionary.cpp
classFileParser.cpp
其中:
-
ClassLoader:负责类文件查找和读取。 -
SystemDictionary:参与类加载、类查找及加载器约束管理。 -
ClassFileParser:负责解析 Class 文件。
从系统工程师角度,需要区分 Java 层的类加载器接口和 HotSpot 内部的类加载实现。两者协同工作,但不是同一个组件。
三、Class 文件如何被 JVM 解析?
Class 文件加载后,HotSpot 需要解析其二进制结构,建立 JVM 内部的数据结构。
3.1 ClassFileParser 的核心职责
源码位置:
src/hotspot/share/classfile/classFileParser.cpp
主要工作:
-
读取 Class 文件头。
-
检查 Class 文件版本。
-
解析常量池。
-
解析类和父类信息。
-
解析字段和方法。
-
解析 Code 属性。
-
创建 JVM 内部类元数据。
例如,常量池中可能包含:
CONSTANT_Class
CONSTANT_Fieldref
CONSTANT_Methodref
CONSTANT_InterfaceMethodref
CONSTANT_String
CONSTANT_NameAndType
这些常量描述了类、字段、方法和字符串等信息。
3.2 常量池解析源码示意
// 对应 HotSpot:
// src/hotspot/share/classfile/classFileParser.cpp
//
// 下面是常量池解析逻辑的教学示意,
// 不是完整的 OpenJDK 原始实现。
for (int index = 1; index < length; index++) {
// 读取当前常量池条目的类型标记
const u1 tag = cfs->get_u1_fast();
switch (tag) {
case JVM_CONSTANT_Class: {
// 读取类名对应的常量池索引
const u2 name_index = cfs->get_u2_fast();
// 将 Class 常量记录到 JVM 内部常量池
cp->klass_index_at_put(index, name_index);
break;
}
case JVM_CONSTANT_Fieldref: {
// 获取字段所属类的索引
const u2 class_index = cfs->get_u2_fast();
// 获取字段名称和描述符的索引
const u2 name_and_type_index = cfs->get_u2_fast();
// 保存字段符号引用
cp->field_at_put(
index,
class_index,
name_and_type_index
);
break;
}
case JVM_CONSTANT_Methodref: {
// 获取方法所属类的索引
const u2 class_index = cfs->get_u2_fast();
// 获取方法名称和描述符的索引
const u2 name_and_type_index = cfs->get_u2_fast();
// 保存方法符号引用
cp->method_at_put(
index,
class_index,
name_and_type_index
);
break;
}
}
}
说明:以上代码用于解释解析流程,省略了其他常量类型、边界检查和异常处理。阅读具体 JDK 版本时,应以对应版本的 classFileParser.cpp 为准。
学习总结:
常量池不是简单的字符串表,而是 JVM 进行类型识别、字段定位、方法调用和动态链接的基础。
3.3 字节码解析与方法 Code 属性
例如:
public class AddDemo {
public static int add(int a, int b) {
// javac 将整数加法转换为 JVM 字节码
return a + b;
}
}
执行:
javap -c -v AddDemo
可以看到类似结果:
public static int add(int, int);
descriptor: (II)I
Code:
stack=2, locals=2, args_size=2
0: iload_0
1: iload_1
2: iadd
3: ireturn
逐条分析:
| 指令 | 含义 | 执行效果 |
|---|---|---|
iload_0 |
加载局部变量 0 | 将 a 压入操作数栈 |
iload_1 |
加载局部变量 1 | 将 b 压入操作数栈 |
iadd |
整数加法 | 弹出两个整数,计算后将结果压栈 |
ireturn |
整数返回 | 将计算结果返回调用者 |
例如,调用 add(10, 20):

这里要特别理解:
-
Code属性保存方法的字节码指令序列。 -
max_stack描述操作数栈所需的最大深度。 -
max_locals描述局部变量表所需的槽位数。 -
descriptor描述方法参数和返回值类型。
HotSpot 解析器负责解析和保存这些信息,而字节码验证器负责进一步检查指令序列是否合法。
学习总结:字节码是 JVM 的抽象指令,操作数栈和局部变量表是字节码执行的基础环境。
四、JVM 如何验证、准备和解析 Class 文件?
Class 文件被解析之后,还需要经过链接过程。
根据 JVMS 第 5 章,类的链接主要包括:
-
Verification(验证)
-
Preparation(准备)
-
Resolution(解析)
4.1 验证:Verification
验证的目的是确保 Class 文件符合 JVM 规范,并且字节码不会破坏 JVM 运行时的类型安全约束。
主要包括四个方面:
| 验证类型 | 检查内容 |
|---|---|
| 文件格式验证 | Magic、版本、常量池结构是否合法 |
| 元数据验证 | 类继承关系、接口关系是否合法 |
| 字节码验证 | 操作数栈、局部变量类型、控制流是否合法 |
| 符号引用验证 | 类、字段、方法的访问是否合法 |
HotSpot 源码:
src/hotspot/share/classfile/verifier.cpp
例如:
public class VerifyDemo {
public static int calculate(int a, int b) {
// 两个 int 类型数据入栈
int result = a + b;
// 返回值类型必须符合方法描述符
return result;
}
}
字节码验证器需要检查:
-
局部变量访问是否符合类型要求。
-
iadd两侧是否为合法的整数操作数。 -
不同控制流分支合并时,操作数栈状态是否兼容。
-
方法返回指令是否符合返回类型要求。
-
操作数栈深度是否超出方法声明的限制。
其中一个重要机制是 StackMapTable。
StackMapTable 保存验证所需的类型状态信息,可以帮助 JVM 验证控制流中的局部变量和操作数栈类型。
系统工程师学习重点:
字节码验证不仅是检查指令是否合法,还涉及控制流分析和数据流类型分析。
4.2 准备:Preparation
准备阶段主要为类的静态字段分配存储空间,并设置默认初始值。
例如:
public class StaticDemo {
// 普通静态变量
static int count = 100;
// 编译期常量
static final int MAX = 200;
static final String NAME = "JVM";
}
对于普通静态变量:
准备阶段:
count = 0
后续类初始化阶段执行 <clinit>(),完成:
count = 100
需要注意:
-
static final编译期常量可能在准备阶段就具有其 ConstantValue 指定的值。 -
普通静态字段通常在准备阶段获得默认值。
-
静态字段的实际存储实现与 HotSpot 版本、字段类型及运行时表示有关。
4.3 解析:Resolution
解析是将 Class 文件中的符号引用转换为 JVM 可以直接使用的运行时引用。
例如:
public class User {
public void say() {
System.out.println("Hello JVM");
}
}
调用:
User user = new User();
// invokevirtual 对应的虚方法调用
user.say();
Class 文件常量池中会记录类似以下信息:
CONSTANT_Class:
User
CONSTANT_Methodref:
User.say:()V
这些信息是符号引用,不是 CPU 内存地址。
JVM 在解析时需要完成:
-
确定目标类。
-
根据方法名称查找方法。
-
根据方法描述符匹配方法。
-
检查访问权限。
-
检查方法调用类型是否合法。
-
建立后续调用需要的运行时信息。
HotSpot 源码:
src/hotspot/share/interpreter/linkResolver.cpp
其方法解析过程可以简化为:
// 对应 HotSpot LinkResolver 的方法解析思想
// 仅为教学示意,不是完整源码。
// 从常量池解析目标类
Klass* resolved_klass = resolve_target_klass(...);
// 从常量池获取方法名称
Symbol* method_name = pool->name_ref_at(index);
// 获取方法描述符
Symbol* signature = pool->signature_ref_at(index);
// 根据字节码指令选择解析逻辑
if (code == Bytecodes::_invokeinterface) {
// 解析接口方法引用
resolve_interface_method(...);
} else {
// 解析类方法引用
resolve_method(...);
}
例如:
| 字节码 | 解析和调用特点 |
|---|---|
invokestatic |
静态方法调用 |
invokespecial |
构造方法、私有方法及特定父类调用 |
invokevirtual |
普通实例虚方法调用 |
invokeinterface |
接口方法调用 |
invokedynamic |
通过动态调用点机制链接目标 |
特别注意:方法符号引用解析和运行时动态分派是两个不同的概念。
例如:
class Parent {
void print() {
System.out.println("Parent");
}
}
class Child extends Parent {
@Override
void print() {
System.out.println("Child");
}
}
public class DispatchDemo {
public static void main(String[] args) {
// 编译期引用类型是 Parent
// 实际对象类型是 Child
Parent obj = new Child();
// invokevirtual 根据实际对象类型进行动态分派
obj.print();
}
}
最终输出:
Child
学习总结:
解析负责建立符号引用与运行时实体之间的联系;动态分派负责根据实际对象类型确定需要执行的方法。两者不能混为一谈。
此外,解析可以采用延迟方式,在实际使用符号引用时才完成,因此不能简单理解为所有符号引用都在类加载时一次性解析完毕。
五、JVM 解释器如何执行字节码?
完成类加载和必要的链接后,JVM 就可以执行方法。
HotSpot 解释器的重点源码:
src/hotspot/share/interpreter/
templateInterpreter.cpp
templateInterpreter.hpp
templateTable.cpp
templateTable.hpp
interpreterRuntime.cpp
5.1 Template Interpreter 的工作原理
HotSpot 使用模板解释器(Template Interpreter)。
其基本执行机制:
-
为字节码指令生成对应的执行模板。
-
生成当前 CPU 架构对应的本地机器码。
-
建立字节码与模板执行入口之间的分派关系。
-
执行当前字节码对应的模板。
-
更新字节码位置和执行状态。
-
继续执行下一条指令。
可以简化为:
读取字节码
|
v
识别字节码操作码
|
v
查找对应的解释器执行入口
|
v
执行模板生成的本地机器码
|
v
更新操作数栈和执行位置
|
v
分派下一条字节码
5.2 TemplateTable 源码分析
源码位置:
src/hotspot/share/interpreter/templateTable.cpp
TemplateTable 负责建立字节码和解释器模板之间的关联。
以 iadd 为例,HotSpot 为整数加法准备相应的解释器执行模板。
需要区分:
-
字节码定义描述
iadd的语义。 -
TemplateTable为指令生成解释器执行代码。 -
具体机器码生成与 CPU 架构相关。
字节码定义可参考:
OpenJDK bytecodes.cpp:iadd 等指令定义。
5.3 TemplateInterpreter 源码分析
重点源码:
src/hotspot/share/interpreter/templateInterpreter.cpp
其中:
void TemplateInterpreter::initialize_code() {
// 初始化抽象解释器相关结构
AbstractInterpreter::initialize();
// 初始化字节码模板
TemplateTable::initialize();
// 后续生成解释器代码和执行入口
// 具体实现还包括入口、返回入口、异常入口等
}
这段代码说明:
-
解释器需要初始化自己的运行环境。
-
字节码模板需要提前建立。
-
JVM 会生成解释器使用的本地代码入口。
这里有一个非常重要的结论:
HotSpot 模板解释器本身就是本地机器码实现的执行引擎。
因此,CPU 并不是直接理解 iload、iadd 这些 Java 字节码,而是在执行解释器生成的机器码。
源码参考:OpenJDK TemplateInterpreter.cpp。
5.4 方法执行帧如何工作?
每次 Java 方法调用,都需要建立相应的栈帧。
栈帧主要包括:
| 组成部分 | 作用 |
|---|---|
| 局部变量表 | 保存参数和局部变量 |
| 操作数栈 | 保存指令执行时的操作数 |
| 动态链接 | 支持运行时常量池引用的解析 |
| 返回信息 | 支持方法返回和调用者恢复执行 |
示例:
public class StackFrameDemo {
public static int add(int a, int b) {
// a、b 对应局部变量表中的槽位
int result = a + b;
// 执行 ireturn 返回整数
return result;
}
public static void main(String[] args) {
// 调用 add 时建立新的方法执行帧
int result = add(10, 20);
System.out.println(result);
}
}
执行过程:
main() 栈帧
|
| 调用 add(10, 20)
v
add() 栈帧
|
| 局部变量表
| a = 10
| b = 20
|
| 操作数栈
| 10 + 20
| 结果 = 30
|
| ireturn
v
返回 main()
|
| result = 30
v
继续执行
注意:栈帧是 JVM 规范定义的抽象执行结构。HotSpot 解释执行时有对应的解释器栈帧实现;JIT 编译后,部分变量可能被分配到寄存器,或者通过优化消除,并不一定保留完全相同的物理布局。
六、JVM 如何通过 JIT 将字节码编译为机器码?
解释器能够执行所有合法的字节码,但对于长期运行、频繁调用的方法,逐条解释通常会带来额外开销。
HotSpot 因此引入即时编译(JIT)。
6.1 JIT 的基本工作流程

这里需要注意:
-
C1 和 C2 是 HotSpot 的两种主要编译器实现。
-
分层编译可以根据代码运行情况选择不同编译级别。
-
不是每个方法都会经过 C1 和 C2。
-
编译完成后,如果优化假设失效,JVM 还可能执行去优化(Deoptimization),返回解释执行或其他适当的执行状态。
6.2 CompileBroker 源码分析
源码位置:
src/hotspot/share/compiler/compileBroker.cpp
CompileBroker 是 HotSpot JIT 编译任务管理的核心组件。
主要负责:
-
接收编译请求。
-
管理编译任务。
-
维护编译任务队列。
-
调度编译线程。
-
调用对应编译器。
-
处理编译结果。
例如,当前 OpenJDK 源码中的初始化逻辑:
// src/hotspot/share/compiler/compileBroker.cpp
// CompileBroker::compilation_init
void CompileBroker::compilation_init(JavaThread* THREAD) {
// 未启用 JIT 时,不初始化编译系统
if (!UseCompiler) {
return;
}
// 注册编译器和相关编译运行环境
// 后续根据编译配置初始化编译线程等资源
// 初始化编译线程
init_compiler_threads();
}
说明:此处摘取当前源码中的核心初始化逻辑,省略了编译器注册、编译线程数量判断及其他初始化细节。C1/C2 的具体初始化路径需要结合编译器构建配置阅读。
源码参考:OpenJDK CompileBroker.cpp。
6.3 C1 和 C2 编译器的区别
| 比较项目 | C1 编译器 | C2 编译器 |
|---|---|---|
| 主要目标 | 较快完成编译 | 生成高质量优化代码 |
| 编译开销 | 相对较低 | 相对较高 |
| 优化程度 | 相对适中 | 通常更深入 |
| 典型用途 | 较早期编译级别 | 高优化级别 |
| 核心源码 | c1/ |
opto/ |
相关源码:
src/hotspot/share/c1/
c1_Compiler.cpp
src/hotspot/share/opto/
c2compiler.cpp
6.4 JIT 如何优化字节码?
例如:
public class InlineDemo {
// 普通方法
public static int square(int value) {
return value * value;
}
public static int calculate() {
// JIT 在满足条件时可能将 square 方法内联
return square(10);
}
}
原始调用关系:
calculate()
|
v
square(10)
|
v
return 10 * 10
经过方法内联和常量传播等优化后,可能简化为:
calculate()
|
v
return 100
这只是说明优化原理的示意,实际优化还受编译上下文、类型信息和运行时假设影响。
重点掌握以下优化技术:
| 优化技术 | 作用 |
|---|---|
| 方法内联 | 减少方法调用开销,扩大优化范围 |
| 常量传播 | 利用已知常量简化表达式 |
| 死代码消除 | 删除无效计算 |
| 循环优化 | 改善循环执行效率 |
| 逃逸分析 | 分析对象是否逃逸出特定作用域 |
| 标量替换 | 在适用情况下消除对象分配 |
| 去虚拟化 | 将部分动态调用转为更容易优化的调用 |
学习总结:JIT 不只是将字节码翻译成机器码,还会利用运行时信息对程序进行动态优化。
七、字节码最终如何被 CPU 执行?
这是从 JVM 软件实现延伸到硬件体系结构的关键部分。
7.1 字节码与机器码的区别
假设 Java 代码:
public class CpuDemo {
public static int add(int a, int b) {
// JVM 字节码层面的整数加法
return a + b;
}
}
对应字节码:
iload_0
iload_1
iadd
ireturn
假设在某个 x86-64 平台上,JIT 最终生成类似以下机器指令:
; x86-64 示意代码
; 假设使用 System V AMD64 调用约定
mov eax, edi ; 读取第一个 int 参数
add eax, esi ; 将第二个 int 参数加到 eax
ret ; 返回计算结果
上述代码仅用于理解,不代表 HotSpot 必然生成完全相同的指令。
如果运行在 AArch64 平台,可能生成类似:
; AArch64 示意代码
add w0, w0, w1 ; 两个 int 参数相加,结果写入 w0
ret ; 返回
因此:
-
同一份 Class 文件可以在不同架构上运行。
-
不同 JVM 实现可以采用不同执行策略。
-
同一个 Java 方法可能生成不同的机器指令。
-
机器指令的数量和排列不必与字节码一一对应。
7.2 解释器和 JIT 两条执行路径

这里有一个容易忽略的区别:
解释器执行时,CPU 执行的是解释器模板生成的机器码;JIT 执行时,CPU 执行的是编译后的 Java 方法机器码。
7.3 不同 CPU 架构的执行差异
| 比较项目 | x86-64 | AArch64 |
|---|---|---|
| 指令集架构 | x86-64 | ARM 64 位 |
| 指令长度 | 可变长度 | 固定 32 位 |
| 通用寄存器 | RAX、RBX 等 | X0-X30 |
| 指令生成 | x86-64 后端 | AArch64 后端 |
| 本地机器码 | x86-64 指令 | AArch64 指令 |
对于系统工程师,还需要继续研究:
-
CPU 指令解码与执行。
-
寄存器分配。
-
内存访问与缓存行为。
-
分支预测。
-
内存屏障。
-
JIT 生成代码的指令缓存一致性。
这些知识能够帮助分析为什么同一份 Java 程序在不同 CPU、不同 JVM 和不同运行参数下具有不同的性能表现。
八、通过 JVM 参数观察加载、解析和执行过程
建议使用实际命令验证前面学习的知识。
8.1 查看 Class 文件结构
# 查看反汇编指令
javap -c BytecodeDemo
# 查看完整 Class 文件结构
javap -v BytecodeDemo
# 查看方法签名
javap -s BytecodeDemo
重点观察:
-
Constant Pool
-
Method Descriptor
-
Code
-
Stack
-
Locals
-
Exception Table
-
StackMapTable
8.2 观察类加载过程
# 输出类加载日志
java -Xlog:class+load=info BytecodeDemo
可以观察类加载顺序、类加载器以及类来源等信息。
8.3 观察解释器和 JIT 编译
# 主要使用解释器执行
java -Xint BytecodeDemo
# 使用编译模式,尝试尽早编译方法
java -Xcomp BytecodeDemo
# 输出 JIT 编译信息
java -XX:+PrintCompilation BytecodeDemo
对于现代 JDK,建议结合:
java -XX:+UnlockDiagnosticVMOptions \
-XX:+LogCompilation \
-XX:LogFile=jit.log \
BytecodeDemo
分析编译日志。
8.4 观察 JIT 生成的机器码
可以使用 HotSpot 的诊断选项:
# 输出汇编相关信息
java -XX:+UnlockDiagnosticVMOptions \
-XX:+PrintAssembly \
BytecodeDemo
注意:
-
PrintAssembly通常需要配合适用的反汇编器支持。 -
生产环境不建议直接开启大量诊断输出。
-
-Xint、-Xcomp适合研究执行模式,不适合直接作为公平的性能对比方法。
建议进一步使用 JMH 进行基准测试。
九、OpenJDK 源码阅读路线
建议不要一次性阅读所有 HotSpot 源码,而是沿着真实执行链路逐步深入。
以下以 OpenJDK 主线 jdk 仓库为参考。
| 顺序 | 源码文件 | 重点研究内容 |
|---|---|---|
| 1 | classLoader.cpp |
Class 文件获取 |
| 2 | systemDictionary.cpp |
类查找、加载与约束 |
| 3 | classFileParser.cpp |
Class 文件解析 |
| 4 | verifier.cpp |
字节码验证 |
| 5 | constantPool.cpp |
运行时常量池 |
| 6 | linkResolver.cpp |
字段和方法解析 |
| 7 | templateInterpreter.cpp |
解释器初始化 |
| 8 | templateTable.cpp |
字节码模板生成 |
| 9 | interpreterRuntime.cpp |
解释器运行时支持 |
| 10 | compileBroker.cpp |
JIT 编译任务管理 |
| 11 | c1_Compiler.cpp |
C1 编译器 |
| 12 | c2compiler.cpp |
C2 编译器 |
重点源码入口:
-
ClassFileParser.cpp:可以从
parse_constant_pool_entries()开始阅读。源码中可以看到 JVM 如何根据常量池 tag 读取不同结构,并写入内部ConstantPool。GitHub
-
verifier.cpp:研究字节码验证。
-
linkResolver.cpp:研究方法解析、接口解析和访问检查。
-
templateInterpreter.cpp:研究解释器初始化和代码入口。
GitHub
-
templateTable.cpp:研究字节码执行模板。
-
compileBroker.cpp:研究编译任务管理。
GitHub
建议固定一个 JDK 版本,例如 JDK 25,统一使用该版本的规范和源码进行学习。不要将 JDK 8、JDK 17 和当前主线的实现细节直接混在一起。
十、建议形成的学习成果
完成本次学习后,建议自己整理四份技术学习材料:
| 学习成果 | 具体内容 | 目标 |
|---|---|---|
| Class 加载流程图 | 加载、解析、验证、准备、初始化 | 理解类生命周期 |
| 字节码执行分析 | iload、iadd、ireturn |
理解解释执行 |
| HotSpot 源码注释 | Parser、Verifier、Interpreter | 掌握源码实现 |
| JIT 执行分析 | C1、C2、Code Cache、机器码 | 理解性能优化 |
推荐进一步进行的实验
可以编写一个完整的小程序:
public class JvmExecutionStudy {
static int counter = 100;
public static int add(int a, int b) {
return a + b;
}
public static int calculate(int value) {
// 普通方法调用
int result = add(value, counter);
// 返回计算结果
return result;
}
public static void main(String[] args) {
// 多次执行,便于观察运行时编译行为
for (int i = 0; i < 100_000; i++) {
int result = calculate(i);
if (result < 0) {
System.out.println(result);
}
}
}
}
建议按照以下步骤实验:
-
使用
javap -c -v分析calculate()的字节码。 -
找到
add()对应的invokestatic调用指令。 -
阅读
ClassFileParser,研究方法 Code 属性如何被解析。 -
阅读
Verifier,研究方法字节码如何验证。 -
阅读
LinkResolver,研究方法引用如何解析。 -
阅读
TemplateTable,研究解释器如何执行整数加法。 -
使用
PrintCompilation观察方法编译情况。 -
使用
PrintAssembly观察编译后的机器码。
十一、最终学习总结:系统工程师必须掌握的核心结论
- Class 文件加载解决的是程序如何进入 JVM 的问题。
类加载器获取二进制数据,HotSpot 将其解析为内部运行时结构。类加载器还决定类型身份和类隔离边界。
- Class 文件解析解决的是程序结构如何转换为 JVM 内部表示的问题。
ClassFileParser 解析常量池、字段、方法、属性和字节码,为后续执行建立基础。
- 字节码验证和链接解决的是程序是否合法、引用是否有效的问题。
Verifier 检查类型安全,Preparation 准备静态字段,Resolution 建立符号引用与运行时实体的联系。
- 解释器解决的是字节码如何执行的问题。
Template Interpreter 通过字节码分派机制,调用对应的本地机器码执行模板。
- JIT 解决的是热点代码如何高效执行的问题。
C1、C2 利用运行时信息进行编译优化,生成适合当前 CPU 的机器码。
- CPU 解决的是本地机器指令如何被硬件执行的问题。
CPU 不直接识别 Java 字节码,而是执行 JVM 解释器或 JIT 编译器生成的机器指令。
最重要的技术认知:
Class 文件提供统一的 JVM 字节码表示,HotSpot 通过类加载、字节码解析、验证、链接、解释执行和 JIT 编译,将 Java 程序逐步转换为硬件能够执行的本地指令。
对于系统工程师来说,真正需要打通的是三层:
-
JVM 规范层: 理解 Class 文件格式、字节码语义和执行模型。
-
HotSpot 实现层: 理解 ClassFileParser、Verifier、TemplateInterpreter、CompileBroker 和 C1/C2。
-
硬件执行层: 理解机器指令、寄存器、缓存、内存访问及 CPU 执行机制。
只有将这三层联系起来,才能真正理解 Java 程序从 .class 文件到 CPU 执行的完整过程。# 30-Java字节码学习总结-字节码如何加载、解析和执行