资讯详情

Java源码编译机制总结

📅 2026/11/2 6:34:48 | 华诺云谱 👁 阅读
Java源码编译机制总结
Java源码编译机制总结前言从系统工程师角度理解Java编译机制一、学习目标二、为什么 Java 源码需要经过 javac 编译2.1 Java 源码是高级语言不是 CPU 能直接执行的指令2.2 为什么不直接把 Java 源码编译成 CPU 机器码三、Class 文件为什么是 Java 的中间表示3.1 Class 文件承担标准化中间表示的职责3.2 Class 文件为什么能够跨平台四、javac 编译器内部究竟做了什么4.1 javac 编译流程4.2 第一阶段词法分析和语法分析4.3 第二阶段语义分析和类型检查4.4 第三阶段语言结构转换Desugar4.5 第四阶段字节码生成4.6 第五阶段ClassWriter 写出 Class 文件五、Class 文件内部到底保存了什么5.1 魔数标识 Class 文件5.2 版本信息决定 JVM 是否支持5.3 常量池保存符号信息5.4 Code 属性保存方法的 JVM 指令六、为什么 Class 文件不直接生成 CPU 机器码6.1 JVM 通过解释器实现字节码执行6.2 JVM 通过 JIT 编译器生成本地机器码6.3 不同 CPU 架构可以采用不同的执行策略七、动手实验观察 javac 如何生成 Class 文件7.1 编写 Java 源码7.2 执行 javac 编译7.3 使用 javap 分析 Class 文件7.4 观察 Class 文件的二进制结构八、从 OpenJDK 源码角度建立学习索引九、从系统工程师角度总结 Class 文件存在的设计价值9.1 编译器与 JVM 解耦9.2 JVM 可以对 Class 文件进行安全性检查9.3 Class 文件使运行时优化成为可能十、最终学习总结需要掌握的核心知识本次学习的核心结论十一、建议后续深入学习的方向前言本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限文中内容难免存在疏漏恳请读者不吝指正。从系统工程师角度理解Java编译机制一、学习目标从系统工程师的角度理解 Java 编译过程不能仅仅停留在.java 文件 → javac → .class 文件而应该进一步思考为什么 Java 源码不能直接交给 JVM 执行为什么 Java 需要设计 Class 文件这种中间表示javac 在编译过程中完成了哪些工作Class 文件为什么能够实现跨平台Class 文件如何被 JVM 加载、验证、解释执行和 JIT 编译Class 文件在 JVM、操作系统和 CPU 之间承担什么职责核心认识Class 文件是 Java 编译器与 JVM 之间的一种标准化二进制契约。Java 通过将高级语言源码转换为与具体 CPU 指令集无关的字节码使编译过程与最终机器指令执行过程解耦。对于系统工程师而言理解 Class 文件的意义实际上是在理解 Java 如何实现语言层、虚拟机层和硬件执行层之间的分层设计。二、为什么 Java 源码需要经过 javac 编译2.1 Java 源码是高级语言不是 CPU 能直接执行的指令例如编写一个简单的 Java 程序publicclassHelloWorld{publicstaticvoidmain(String[]args){inta10;intb20;intresultab;System.out.println(result);}}从开发人员角度看这段代码表达的是定义一个类。定义一个 main 方法。创建两个整数变量。执行加法运算。输出计算结果。但是从 CPU 角度看它无法直接理解intresultab;CPU 真正执行的是与具体处理器指令集相关的机器指令例如x86-64 指令。AArch64 指令。其他处理器架构对应的机器指令。Java 源码与 CPU 指令之间存在明显的抽象层次差异。因此需要一个转换过程将 Java 高级语言转换为 JVM 能够识别的执行指令。这个过程就是 Java 编译。2.2 为什么不直接把 Java 源码编译成 CPU 机器码如果 javac 直接把 Java 源码编译成某一种 CPU 的机器码就会产生一个问题编译结果将与目标处理器架构绑定。例如Java 源码 | v javac | ---- x86-64 机器码 | ---- AArch64 机器码 | ---- 其他 CPU 机器码这种模式意味着x86-64 平台需要生成 x86-64 机器码。ARM 平台需要生成 ARM 机器码。更换 CPU 架构可能需要重新编译。不同平台需要维护不同的编译目标。Java 采用了另一种设计Java 源码 | v javac 编译器 | v Class 文件平台无关的字节码 | ------------------ | | v v JVMx86-64 JVMAArch64 | | v v x86-64 机器码 ARM 机器码javac 不需要直接决定最终使用哪种 CPU 指令。它只需要生成符合 JVM 规范的 Class 文件。不同平台的 JVM 再负责将字节码转换为当前处理器可以执行的机器指令。学习重点Java 将编译工作分成两个层次静态编译阶段 javac 将 Java 源码转换为 JVM 字节码。运行时执行阶段 JVM 根据运行环境解释执行字节码或者通过 JIT 编译为本地机器码。这种设计实现了编译器与处理器架构之间的解耦。三、Class 文件为什么是 Java 的中间表示3.1 Class 文件承担标准化中间表示的职责Class 文件不是简单的 Java 源码压缩文件也不是 CPU 机器码。它是一种具有明确结构和语义的二进制文件。根据 JVMSJava Virtual Machine Specification规范Class 文件包含组成部分主要职责魔数Magic标识这是一个 Class 文件版本信息标识 Class 文件格式版本常量池保存类、方法、字段、字符串等符号信息访问标志描述类的访问属性类与接口信息描述当前类及其继承关系字段表描述类的成员变量方法表描述方法及其相关属性属性表保存 Code、SourceFile、Signature 等扩展信息Class 文件的设计使得 JVM 不需要理解 Java 源码的语法结构。JVM 只需要按照 Class 文件规范解析二进制数据就能够理解类的结构和方法的执行逻辑。3.2 Class 文件为什么能够跨平台假设有一段 Java 源码intresult1020;javac 编译之后可以形成类似下面的 JVM 字节码bipush 10 bipush 20 iadd istore_1这些指令并不属于 x86 或 ARM 指令集。它们属于 JVM 指令集。例如JVM 字节码含义bipush将一个 byte 范围的整数压入操作数栈iadd执行两个 int 类型整数加法istore_1将操作数栈顶部的 int 保存到局部变量表索引 1x86-64 JVM 和 AArch64 JVM 都可以识别这些 JVM 指令。但是它们最终可能采用不同的机器指令完成计算。因此跨平台的不是 CPU 机器码而是 JVM 规范定义的 Class 文件格式和字节码语义。需要注意跨平台并不意味着任何 Class 文件都能在任何环境中无条件运行。还需要考虑JVM 是否支持对应的 Class 文件版本。运行环境是否具备所需的类库。操作系统是否提供相应的系统资源。是否存在平台相关的 JNI 或本地库依赖。四、javac 编译器内部究竟做了什么从 OpenJDK 实现角度看javac 并不是简单地把 Java 代码转换成字节码而是经历了一系列编译处理阶段。4.1 javac 编译流程Java 源码.java开发人员编写的高级语言程序1. 词法分析与语法分析Scanner / Parser生成抽象语法树 AST2. 符号进入与语义分析Enter / Attr / Check / Resolve建立符号关系、完成类型检查和名称解析3. 注解处理与编译转换Annotation Processing / TransTypes / Lower 等处理注解及语言结构转换4. 字节码生成Gen将编译后的程序表示转换成 JVM 指令5. Class 文件写出ClassWriter组织常量池、字段、方法、Code 等结构Class 文件.class符合 JVM 规范的二进制文件以上是便于理解的逻辑流程不代表 javac 内部所有阶段严格按照单一线性顺序执行。4.2 第一阶段词法分析和语法分析假设 Java 源码intresult1020;词法分析会识别出int result 10 20 ;语法分析则会根据 Java 语法规则构建抽象语法树AST。可以简化表示为VariableDeclaration | ---- Type: int | ---- Name: result | ---- Initializer | ---- BinaryExpression () | ---- 10 | ---- 20系统工程师学习重点AST 描述的是程序的语法结构而不是 CPU 的执行指令。编译器需要先理解程序的结构才能进一步分析变量类型、方法调用、表达式和控制流程。4.3 第二阶段语义分析和类型检查例如inta10;StringbJava;intresultab;这段代码在语法层面可以构建 AST但在语义层面存在类型不匹配问题。javac 需要检查变量是否已经声明。变量类型是否匹配。方法是否存在。方法参数是否匹配。访问权限是否允许。类和接口之间的继承关系是否合法。只有通过必要的语义检查编译器才能继续生成有效的编译结果。学习重点javac 不仅负责转换程序形式还负责在编译阶段发现大量程序错误。这可以减少运行时才发现错误的概率。4.4 第三阶段语言结构转换DesugarJava 语言提供了很多高级语法结构例如Lambda 表达式。泛型。增强 for 循环。内部类。自动装箱和拆箱。字符串拼接。这些语言结构并不一定以源码中的原始形式存在于最终字节码中。例如ListStringlistnewArrayList();for(Stringvalue:list){System.out.println(value);}增强 for 循环在编译过程中会被转换为迭代器相关的操作针对普通 Iterable 集合。泛型则主要通过类型擦除实现传统 JVM 类型表示。需要注意Lambda 通常通过invokedynamic等机制实现并不意味着每个 Lambda 都必然被转换成传统匿名内部类。学习重点Java 源码中的语言结构与最终 Class 文件中的字节码结构并不是一一对应的。编译器承担了高级语言特性向 JVM 可执行表示转换的职责。4.5 第四阶段字节码生成这是 javac 最核心的工作之一。OpenJDK 中负责这项工作的主要组件是src/jdk.compiler/share/classes/com/sun/tools/javac/jvm/Gen.java该类的源码注释明确说明/** * This pass maps flat Java (i.e. without inner classes) to bytecodes. * * 学习注释 * Gen 负责将经过前序编译处理的 Java 程序表示 * 转换为 JVM 字节码。 * * 注意 * 这里生成的是 JVM 指令不是 x86 或 ARM 机器指令。 */publicclassGenextendsJCTree.Visitor{// 省略其他实现}源码参考OpenJDK Gen.java。继续看一个简单例子。Java 源码publicclassCalculator{publicstaticintadd(inta,intb){returnab;}}对应的 JVM 字节码可以通过以下命令查看javac Calculator.java javap-c-vCalculator.class方法的核心字节码类似public static int add(int, int); Code: 0: iload_0 1: iload_1 2: iadd 3: ireturn逐条解释字节码作用iload_0将局部变量 0 中的 int 压入操作数栈iload_1将局部变量 1 中的 int 压入操作数栈iadd从操作数栈取出两个 int 并执行加法ireturn返回 int 结果这里需要理解 JVM 的一个重要设计JVM 字节码采用基于操作数栈的执行模型。方法执行时JVM 使用局部变量表和操作数栈来组织数据及计算过程。例如局部变量表 index 0 a index 1 b iload_0 | v 操作数栈[a] iload_1 | v 操作数栈[a, b] iadd | v 操作数栈[a b] ireturn | v 返回计算结果这与 CPU 寄存器执行模型存在区别。最终 JVM 可以通过解释器或者 JIT 编译器将这些字节码转换为当前 CPU 的执行指令。4.6 第五阶段ClassWriter 写出 Class 文件生成字节码之后还需要将类的完整信息组织成符合规范的二进制文件。OpenJDK 中负责这项工作的主要组件是src/jdk.compiler/share/classes/com/sun/tools/javac/jvm/ClassWriter.java核心职责包括写入 Class 文件头。写入版本信息。写入常量池。写入类访问标志。写入字段信息。写入方法信息。写入 Code 属性。写入其他属性。下面通过简化代码说明// OpenJDKcom.sun.tools.javac.jvm.ClassWriter// 以下是职责示意不是完整原始方法实现。publicclassClassWriter{voidwriteClassFile(OutputStreamout,ClassSymbolc)throwsIOException{// 1. 写入 Class 文件的魔数、版本等信息。// 2. 写入常量池。// 3. 写入类名、父类、接口等信息。// 写入字段信息。writeFields(c.members());// 写入方法信息。// 包含方法描述符及方法对应的 Code 属性等。writeMethods(c.members());// 4. 写入类级别属性。// 5. 将组织好的二进制内容输出。}}源码参考OpenJDK ClassWriter.java。学习重点Gen 负责生成 JVM 字节码ClassWriter 负责将字节码及相关类元数据组织并写入 Class 文件。两者职责不同不应该把 Class 文件简单理解成字节码指令的集合。五、Class 文件内部到底保存了什么根据 JVMS 第 4 章Class 文件有明确的二进制结构。其基本结构如下// 根据 JVMS §4.1 ClassFile 结构整理。// 这是规范结构示意不是 Java 源码。ClassFile{u4 magic;// 魔数0xCAFEBABEu2 minor_version;// 次版本号u2 major_version;// 主版本号u2 constant_pool_count;// 常量池数量cp_info constant_pool[];// 常量池u2 access_flags;// 类访问标志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[];// 类属性}参考JVMS Chapter 4Class File Format。5.1 魔数标识 Class 文件Class 文件的前四个字节是CA FE BA BE也就是0xCAFEBABEJVM 通过魔数判断输入文件是否具有正确的 Class 文件格式标识。它类似于操作系统和其他二进制文件格式中的文件标识。5.2 版本信息决定 JVM 是否支持Class 文件包含主版本号和次版本号。例如Java 8 - major version 52 Java 11 - major version 55 Java 17 - major version 61 Java 21 - major version 65 Java 25 - major version 69如果使用较新的 javac 生成了较高版本的 Class 文件而运行环境中的 JVM 不支持该版本就可能出现java.lang.UnsupportedClassVersionError从系统工程师角度看这意味着Class 文件版本是 Java 应用部署时必须考虑的兼容性边界。5.3 常量池保存符号信息常量池负责保存编译结果中需要引用的各种常量和符号信息。例如publicclassCalculator{publicstaticintadd(inta,intb){returnab;}}常量池中可能包含Calculator java/lang/Object add (II)I Code LineNumberTable SourceFile其中(II)I表示第一个 I第一个 int 参数。第二个 I第二个 int 参数。最后一个 I返回 int。常量池为字节码中的符号引用提供基础。例如调用其他类的方法时字节码可以通过常量池索引引用目标方法而不需要直接把目标方法在内存中的地址写死。这也为 JVM 后续的符号引用解析提供了基础。5.4 Code 属性保存方法的 JVM 指令方法的执行逻辑保存在 Code 属性中。其核心信息包括max_stack操作数栈所需的最大深度。max_locals局部变量表所需的槽位数量。code_length字节码长度。code实际 JVM 指令。exception_table异常处理信息。其他属性例如 StackMapTable、LineNumberTable。这里需要特别注意不是所有方法都有 Code 属性。例如publicnativevoidexecute();publicabstractvoidprocess();native 方法和 abstract 方法不包含普通 Java 方法体对应的 Code 属性。这也是 JVM 规范对 Class 文件结构的要求。参考JVMS §4.7.3 Code Attribute。六、为什么 Class 文件不直接生成 CPU 机器码这是本次学习最重要的系统设计问题。6.1 JVM 通过解释器实现字节码执行Class 文件被 JVM 加载后解释器可以逐条执行 JVM 字节码。例如JVM 字节码 iload_0 iload_1 iadd ireturn解释器负责识别指令并执行对应的操作。在 HotSpot JVM 中解释器有自己的字节码分发和执行机制。因此即使没有将方法编译成本地机器码Java 程序也能够运行。6.2 JVM 通过 JIT 编译器生成本地机器码如果某个方法被频繁执行JVM 可以根据运行时信息对其进行 JIT 编译。例如Class 文件 | v JVM 字节码 | v 解释执行 | v 热点方法识别 | v JIT 编译 | v 本地机器码 | v CPU 执行JIT 编译器可以利用运行时信息实施优化例如方法内联。常量传播。公共子表达式消除。死代码消除。循环优化。基于类型反馈的优化。这意味着 Java 不仅仅依赖静态编译还能够根据真实运行情况进行动态优化。6.3 不同 CPU 架构可以采用不同的执行策略例如同样的字节码iload_0 iload_1 iadd ireturn在不同平台上最终生成的机器码可能不同执行平台最终执行指令x86-64x86-64 指令集AArch64ARM 64 位指令集即使是相同 CPU 架构不同 JVM 实现、不同 JIT 编译器、不同运行时参数也可能导致不同的机器码和执行性能。系统工程师学习重点Class 文件提供统一的字节码语义而 JVM 根据目标 CPU 架构、运行时状态和优化策略决定最终执行路径。七、动手实验观察 javac 如何生成 Class 文件建议通过一个完整实验把前面的理论与实际编译结果对应起来。7.1 编写 Java 源码创建Calculator.javapublicclassCalculator{// 静态变量privatestaticfinalintDEFAULT_VALUE10;// 实例变量privateintvalue;// 构造方法publicCalculator(intvalue){this.valuevalue;}// 普通方法publicintadd(inta,intb){// 局部变量intresultab;returnresult;}// 静态方法publicstaticintmultiply(inta,intb){returna*b;}publicstaticvoidmain(String[]args){CalculatorcalculatornewCalculator(DEFAULT_VALUE);intresultcalculator.add(10,20);System.out.println(result);}}7.2 执行 javac 编译# 创建 Class 文件输出目录mkdirout# 编译 Java 源码javac-g-dout Calculator.java参数说明参数作用-g生成调试信息-d out指定 Class 文件输出目录编译完成后out/ Calculator.classjavac 的标准行为是将 Java 源文件编译为 JVM 可运行的 Class 文件。可以通过-d指定输出目录。7.3 使用 javap 分析 Class 文件执行javap-v-pout/Calculator.class重点观察以下内容Classfile ... minor version: ... major version: ... Constant pool: #1 Methodref ... #2 Class ... #3 Fieldref ... { private static final int DEFAULT_VALUE; private int value; public Calculator(int); public int add(int, int); public static int multiply(int, int); public static void main(java.lang.String[]); }建议重点研究major versionClass 文件版本。Constant pool常量池结构。Field成员变量定义。Method方法定义。descriptor方法描述符。Code方法字节码。StackMapTable字节码验证相关的栈映射信息。LineNumberTable源码行号与字节码位置的映射。继续执行# 仅查看字节码javap-cout/Calculator.class# 查看方法描述符和详细结构javap-s-vout/Calculator.class# 运行程序java-cpout Calculator预期输出307.4 观察 Class 文件的二进制结构还可以使用# Linuxxxd-g1out/Calculator.class|head-n20Windows 可以使用Format-Hex .\out\Calculator.class观察开头的字节CA FE BA BE这就是 Class 文件魔数。通过这个实验可以将以下几个层次联系起来Calculator.java | | javac v Calculator.class | | javap -v v Class 文件结构 | | java v JVM 加载与执行 | v 程序输出 30八、从 OpenJDK 源码角度建立学习索引作为系统工程师建议不要只学习 javac 的使用方法还要进一步阅读 OpenJDK 的编译器实现。以下以 OpenJDK 主线源码目录为参考源码文件主要职责建议研究内容JavaCompiler.java编译流程协调编译阶段如何组织Parser.java语法分析如何生成 ASTEnter.java符号进入如何建立类和成员符号Attr.java属性与类型分析类型检查和语义分析TransTypes.java泛型相关转换类型擦除Lower.java语言结构转换高级语法如何转换Gen.java字节码生成AST 如何转换成 JVM 指令ClassWriter.javaClass 文件写出如何序列化 Class 文件源码目录src/jdk.compiler/share/classes/com/sun/tools/javac/ main/ JavaCompiler.java parser/ Parser.java comp/ Enter.java Attr.java TransTypes.java Lower.java jvm/ Gen.java ClassWriter.java建议按照以下顺序阅读第一步JavaCompiler.java理解 javac 如何协调整个编译过程。第二步Parser.java 和 Attr.java理解 Java 源码如何形成 AST以及如何进行类型检查。第三步Lower.java 和 TransTypes.java理解高级语言特性如何转换成 JVM 更容易处理的表示。第四步Gen.java重点研究 Java 表达式、方法调用、条件分支如何生成字节码。第五步ClassWriter.java研究常量池、方法表、Code 属性等结构如何被写入 Class 文件。注意这些都是 javac 内部实现不属于稳定的公开 API阅读时应选择与实际使用 JDK 版本对应的源码。九、从系统工程师角度总结 Class 文件存在的设计价值Class 文件并不是 Java 编译器偶然产生的一种文件而是 Java 虚拟机架构的重要组成部分。可以从六个方面理解其设计价值。设计目标Class 文件解决的问题平台解耦不直接依赖特定 CPU 指令集标准化统一编译器和 JVM 之间的数据格式可验证性JVM 可以检查文件结构和字节码约束可链接性通过符号引用支持类之间的关联可扩展性通过属性机制承载额外元数据信息运行时优化为解释器和 JIT 提供统一输入9.1 编译器与 JVM 解耦javac 只需要按照 JVM 规范生成 Class 文件。JVM 不需要知道 Java 源码最初是由什么开发工具编写的。只要其他语言的编译器能够生成符合 JVM 规范的 Class 文件理论上也可以在 JVM 上运行。例如Java。Kotlin。Scala。Groovy。这也是 JVM 不等同于 Java 语言的原因。9.2 JVM 可以对 Class 文件进行安全性检查Class 文件不是生成之后就一定可以安全执行。JVM 加载 Class 文件时还需要执行格式检查、字节码验证和符号引用解析等工作。例如检查魔数。检查 Class 文件版本。检查常量池结构。检查方法描述符。检查字节码指令是否合法。检查操作数栈使用是否满足验证约束。这为 JVM 的可靠运行提供了基础。9.3 Class 文件使运行时优化成为可能Class 文件提供了相对稳定的中间表示。JVM 可以在运行过程中根据实际情况解释执行。识别热点方法。JIT 编译。执行运行时优化。根据运行时反馈进行去优化Deoptimization。这使 Java 能够兼顾跨平台能力和运行时性能。十、最终学习总结需要掌握的核心知识本次学习的核心结论1. 为什么生成 Class 文件因为 Java 源码属于高级语言不能直接被 CPU 执行。需要将其转换为 JVM 能够识别的标准化中间表示。2. Class 文件是什么Class 文件是符合 JVM 规范的二进制文件包含类结构、常量池、字段、方法、字节码和属性等信息。3. javac 做什么javac 完成词法分析、语法分析、语义分析、类型检查、语言结构转换、字节码生成和 Class 文件写出。4. 为什么能够跨平台因为 Class 文件使用 JVM 字节码而不是某一种 CPU 的机器指令。不同平台的 JVM 负责适配各自的处理器架构。5. JVM 如何执行 Class 文件JVM 加载并验证 Class 文件通过解释器执行字节码或者通过 JIT 编译器生成本地机器码。6. 系统工程师应该关注什么不仅要理解 Java 编译原理还要关注 Class 文件版本兼容性、类加载、字节码验证、JIT 编译、CPU 架构适配和运行时性能。十一、建议后续深入学习的方向结合这次学习建议沿着下面的知识链继续深入Java 源码与 javac 编译理解 AST、类型检查、泛型擦除、语法转换和字节码生成。本次学习Class 文件结构深入分析研究常量池、方法描述符、字段表、方法表、Code 属性和 StackMapTable。JVM 类加载机制研究 ClassLoader、双亲委派、加载、验证、准备、解析和初始化。JVM 字节码执行机制研究解释器、字节码分发、操作数栈、局部变量表和方法调用。JIT 编译与本地机器码研究 C1、C2、分层编译、内联、OSR 和 Deoptimization。HotSpot 与 CPU 架构研究 x86-64、AArch64 的机器指令生成、寄存器分配、内存屏障和执行性能。最终需要建立的系统工程师认知Java 源码 | | javac语言理解与静态编译 v Class 文件 | | JVM加载、验证、链接 v JVM 字节码 | | 解释器 / JIT v 本地机器码 | | 操作系统与处理器 v CPU 执行Java 的核心设计思想不是让所有 CPU 理解 Java而是通过 Class 文件和 JVM 建立统一的执行抽象层。这就是 Java 源码经过 javac 编译后需要生成 Class 文件的根本原因。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑