Java字节码学习总结:字节码如何加载、解析和执行

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 源码,理解:

  1. Class 文件如何被类加载器读取并转换为 JVM 内部数据结构。

  2. JVM 如何解析常量池、字段、方法和 Code 属性。

  3. JVM 如何验证字节码的合法性和类型安全。

  4. JVM 如何将符号引用解析为运行时可以使用的引用。

  5. 解释器如何通过模板分派机制执行字节码。

  6. JIT 如何将热点字节码编译为本地机器码。

  7. 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

主要工作:

  1. 读取 Class 文件头。

  2. 检查 Class 文件版本。

  3. 解析常量池。

  4. 解析类和父类信息。

  5. 解析字段和方法。

  6. 解析 Code 属性。

  7. 创建 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;
    }
}

字节码验证器需要检查:

  1. 局部变量访问是否符合类型要求。

  2. iadd 两侧是否为合法的整数操作数。

  3. 不同控制流分支合并时,操作数栈状态是否兼容。

  4. 方法返回指令是否符合返回类型要求。

  5. 操作数栈深度是否超出方法声明的限制。

其中一个重要机制是 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 在解析时需要完成:

  1. 确定目标类。

  2. 根据方法名称查找方法。

  3. 根据方法描述符匹配方法。

  4. 检查访问权限。

  5. 检查方法调用类型是否合法。

  6. 建立后续调用需要的运行时信息。

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)。

其基本执行机制:

  1. 为字节码指令生成对应的执行模板。

  2. 生成当前 CPU 架构对应的本地机器码。

  3. 建立字节码与模板执行入口之间的分派关系。

  4. 执行当前字节码对应的模板。

  5. 更新字节码位置和执行状态。

  6. 继续执行下一条指令。

可以简化为:

复制代码
读取字节码
    |
    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 编译任务管理的核心组件。

主要负责:

  1. 接收编译请求。

  2. 管理编译任务。

  3. 维护编译任务队列。

  4. 调度编译线程。

  5. 调用对应编译器。

  6. 处理编译结果。

例如,当前 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 编译器

重点源码入口:

建议固定一个 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);
            }
        }
    }
}

建议按照以下步骤实验:

  1. 使用 javap -c -v 分析 calculate() 的字节码。

  2. 找到 add() 对应的 invokestatic 调用指令。

  3. 阅读 ClassFileParser,研究方法 Code 属性如何被解析。

  4. 阅读 Verifier,研究方法字节码如何验证。

  5. 阅读 LinkResolver,研究方法引用如何解析。

  6. 阅读 TemplateTable,研究解释器如何执行整数加法。

  7. 使用 PrintCompilation 观察方法编译情况。

  8. 使用 PrintAssembly 观察编译后的机器码。

十一、最终学习总结:系统工程师必须掌握的核心结论

  1. Class 文件加载解决的是程序如何进入 JVM 的问题。

类加载器获取二进制数据,HotSpot 将其解析为内部运行时结构。类加载器还决定类型身份和类隔离边界。

  1. Class 文件解析解决的是程序结构如何转换为 JVM 内部表示的问题。

ClassFileParser 解析常量池、字段、方法、属性和字节码,为后续执行建立基础。

  1. 字节码验证和链接解决的是程序是否合法、引用是否有效的问题。

Verifier 检查类型安全,Preparation 准备静态字段,Resolution 建立符号引用与运行时实体的联系。

  1. 解释器解决的是字节码如何执行的问题。

Template Interpreter 通过字节码分派机制,调用对应的本地机器码执行模板。

  1. JIT 解决的是热点代码如何高效执行的问题。

C1、C2 利用运行时信息进行编译优化,生成适合当前 CPU 的机器码。

  1. 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字节码学习总结-字节码如何加载、解析和执行

相关推荐
qq_406981031 小时前
c++的decltype是什么
开发语言·c++
坤坤子吖1 小时前
算法学习——单调队列
开发语言·c++·笔记·学习·算法
Flynn Liang1 小时前
大模型怎么自己调函数?Spring AI 2.0 Tool Calling 自动循环原理 + 完整时序图
java·人工智能·spring·agent·函数调用·springai·toolcalling
十年磨一剑~2 小时前
Linux 内核中断处理机制与软硬件协同流程
linux·嵌入式硬件
杜 硕2 小时前
从零开始的C++生活——第六讲——01
开发语言·c++·生活
Ruiery2 小时前
Linux 6.6内核并发与同步(五):mutex 可睡眠锁
linux·运维·服务器
莫晓燚2 小时前
双机部署若依 (RuoYi‑Vue) 传统环境部署---实战
linux·运维·数据库·redis·mysql·nginx·ruoyi
乌暮2 小时前
请求进门先安检:Spring 拦截器与统一功能实战指南
java·开发语言·后端·学习·spring
学长毕设3 小时前
基于SpringBoot的烘焙商品商品系统-附源码
java·spring boot·后端