JVM三种编译器(Javac \ JIT \ AOT)

本文介绍了JVM中从Java源代码到机器码的编译过程。涉及前端编译器(如javac)将源代码转为字节码,JIT编译器把字节码转为机器码,有C1、C2编译模式,还有AOT编译器直接将源代码转为机器码。对比了它们的编译速度和质量,多种方式配合让JVM达最优状态。

JVM基础系列第4讲:从源代码到机器码,发生了什么?

对于 Java 语言来说,其从源代码到机器码,这中间到底发生了什么呢?

如下图所示,编译器可以分为:前端编译器、JIT 编译器和AOT编译器.              


1. 前端编译器:源代码到字节码

之前我们说到:对于 Java 虚拟机来说,其实际输入的是字节码文件,而不是 Java 文件。那么对于 Java 语言而言,其实怎么将 Java 代码转化成字节码文件的呢?我们知道在 JDK 的安装目录里有一个 javac 工具,就是它将 Java 代码翻译成字节码,这个工具我们叫做编译器。相对于后面要讲的其他编译器,其因为处于编译的前期,因此又被成为前端编译器。

                               

通过 javac 编译器,我们可以很方便地将 java 源文件翻译成字节码文件。就拿我们最熟悉的 Hello World 作为例子:

public class Demo{
   public static void main(String args[]){
        System.out.println("Hello World!");
   }
}

我们使用 javac 命令编译上面这个类,便会生成一个 Demo.class 文件:

> javac Demo.java
> ls 
Demo.java Demo.class

我们使用纯文本编辑器打开 Demo.class 文件,我们会发现是一连串的 16 进制二进制流。

                  

我们运行 javac 命令的过程,其实就是 javac 编译器解析 Java 源代码,并生成字节码文件的过程。说白了,其实就是使用 javac 编译器把 Java 语言规范转化为字节码语言规范。javac 编译器的处理过程可以分为下面四个阶段:

第一个阶段:词法、语法分析。在这个阶段,JVM 会对源代码的字符进行一次扫描,最终生成一个抽象的语法树。简单地说,在这个阶段 JVM 会搞懂我们的代码到底想要干嘛。就像我们分析一个句子一样,我们会对句子划分主谓宾,弄清楚这个句子要表达的意思一样。

第二个阶段:填充符号表。我们知道类之间是会互相引用的,但在编译阶段,我们无法确定其具体的地址,所以我们会使用一个符号来替代。在这个阶段做的就是类似的事情,即对抽象的类或接口进行符号填充。等到类加载阶段,JVM 会将符号替换成具体的内存地址。

第三个阶段:注解处理。我们知道 Java 是支持注解的,因此在这个阶段会对注解进行分析,根据注解的作用将其还原成具体的指令集。

第四个阶段:分析与字节码生成。到了这个阶段,JVM 便会根据上面几个阶段分析出来的结果,进行字节码的生成,最终输出为 class 文件。

我们一般称 javac 编译器为前端编译器,因为其发生在整个编译的前期。常见的前端编译器有 Sun 的 javac,Eclipse JDT 的增量式编译器(ECJ)。


2. JIT 编译器:从字节码到机器码

当源代码转化为字节码之后,其实要运行程序,有两种选择。一种是使用 Java 解释器解释执行字节码,另一种则是使用 JIT 编译器将字节码转化为本地机器代码。

这两种方式的区别在于,前者启动速度快但运行速度慢,而后者启动速度慢但运行速度快。至于为什么会这样,其原因很简单。因为解释器不需要像 JIT 编译器一样,将所有字节码都转化为机器码,自然就少去了优化的时间。而当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。所以在实际情况中,为了运行速度以及效率,我们通常采用两者相结合的方式进行 Java 代码的编译执行。

                       

在 HotSpot 虚拟机内置了两个即时编译器,分别称为 Client Compiler 和Server Compiler。这两种不同的编译器衍生出两种不同的编译模式,我们分别称之为:C1 编译模式,C2 编译模式。

注意:现在许多人习惯上将 Client Compiler 称为 C1 编译器,将 Server Compiler 称为 C2 编译器,但在 Oracle 官方文档中将其描述为 compiler mode(编译模式)。所以说 C1 编译器、C2 编译器只是我们自己的习惯性称呼,并不是官方的说法。这点需要特别注意。

那么 C1 编译模式和 C2 编译模式有什么区别呢?

C1 编译模式会将字节码编译为本地代码,进行简单、可靠的优化,如有必要将加入性能监控的逻辑。而 C2 编译模式,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。

简单地说 C1 编译模式做的优化相对比较保守,其编译速度相比 C2 较快。而 C2 编译模式会做一些激进的优化,并且会根据性能监控做针对性优化,所以其编译质量相对较好,但是耗时更长。

那么到底应该选择 C1 编译模式还是 C2 编译模式呢?

实际上对于 HotSpot 虚拟机来说,其一共有三种运行模式可选,分别是:

  • 混合模式(Mixed Mode) 。即 C1 和 C2 两种模式混合起来使用,这是默认的运行模式。如果你想单独使用 C1 模式或 C2 模式,使用 -client 或 -server 打开即可。
  • 解释模式(Interpreted Mode)。即所有代码都解释执行,使用 -Xint 参数可以打开这个模式。
  • 编译模式(Compiled Mode)。 此模式优先采用编译,但是无法编译时也会解释执行,使用 -Xcomp 打开这种模式。

在命令行中输入 java -version 可以看到,我机器上的虚拟机使用 Mixed Mode 运行模式。

            

写到这里,我们了解了从 Java 源代码到字节码,再从字节码到机器码的全过程。本来到这里就应该结束了,但在我们 Java 中还有一个 AOT 编译器,它能直接将源代码转化为机器码。


3. AOT 编译器:源代码到机器码

AOT 编译器的基本思想是:在程序执行前生成 Java 方法的本地代码,以便在程序运行时直接使用本地代码。

但是 Java 语言本身的动态特性带来了额外的复杂性,影响了 Java 程序静态编译代码的质量。例如 Java 语言中的动态类加载,因为 AOT 是在程序运行前编译的,所以无法获知这一信息,所以会导致一些问题的产生。类似的问题还有很多,这里就不一一举例了。

总的来说,AOT 编译器从编译质量上来看,肯定比不上 JIT 编译器。其存在的目的在于避免 JIT 编译器的运行时性能消耗或内存消耗,或者避免解释程序的早期性能开销。

在运行速度上来说,AOT 编译器编译出来的代码比 JIT 编译出来的慢,但是比解释执行的快。而编译时间上,AOT 也是一个始终的速度。所以说,AOT 编译器的存在是 JVM 牺牲质量换取性能的一种策略。就如 JVM 其运行模式中选择 Mixed 混合模式一样,使用 C1 编译模式只进行简单的优化,而 C2 编译模式则进行较为激进的优化。充分利用两种模式的优点,从而达到最优的运行效率。


总结

在 JVM 中有三个非常重要的编译器,它们分别是:前端编译器、JIT 编译器、AOT 编译器。

前端编译器,最常见的就是我们的 javac 编译器,其将 Java 源代码编译为 Java 字节码文件。JIT 即时编译器,最常见的是 HotSpot 虚拟机中的 Client Compiler 和 Server Compiler,其将 Java 字节码编译为本地机器代码。而 AOT 编译器则能将源代码直接编译为本地机器码。这三种编译器的编译速度和编译质量如下:

  • 编译速度上,解释执行 > AOT 编译器 > JIT 编译器。
  • 编译质量上,JIT 编译器 > AOT 编译器 > 解释执行。

而在 JVM 中,通过这几种不同方式的配合,使得 JVM 的编译质量和运行速度达到最优的状态。


来源:https://www.cnblogs.com/chanshuyi/p/jvm_serial_04_from_source_code_to_machine_code.html

 

03-21
<think>好的,我现在需要向用户介绍JIT(即时编译器)。首先,用户可能对JIT不太了解,所以要从基础讲起。JITJava性能优化的关键部分,但很多人可能只听说过名字,不清楚具体原理。 用户之前的问题是关于线程join和锁的关系,现在转向JIT,可能是在学习Java底层机制,或者对性能优化感兴趣。我需要确保解释清晰,结构合理,符合用户之前要求的层次化解析。 首先,应该定义JIT是什么,全称是什么,属于哪个环节。JIT全称Just-In-Time Compiler,属于JVM的一部分,负责在运行时将字节码编译成本地机器码。和传统的解释执行相比,JIT能提高效率,但需要区分与AOT(预先编译)的区别。 接下来需要解释JIT的工作原理,比如热点代码检测,编译触发条件,分层编译(如C1、C2编译器)。这部分可能需要用例子说明,比如方法被多次调用后触发编译,提高执行速度。 然后要对比解释执行和编译执行的差异,说明JIT的优势,比如减少解释器的开销,优化代码路径,内联等方法。同时也要提到JIT的缺点,比如启动时的性能损耗,内存占用增加。 用户可能想知道JITJava应用中的实际影响,比如如何影响启动时间和长期运行性能。这时候可以提到服务器应用和长时间运行的应用更适合利用JIT,而短生命周期的程序可能收益不大。 还需要涵盖JIT的优化技术,比如方法内联、逃逸分析、循环优化、去虚拟化等。用具体的例子说明这些优化如何提升性能,例如逃逸分析导致栈上分配对象,减少GC压力。 此外,用户可能对如何监控和调整JIT感兴趣,可以介绍JVM参数,如-XX:+PrintCompilation查看编译日志,-XX:+UnlockExperimentalVMOptions -XX:+PrintInlining查看内联决策。同时,分层编译的策略(如使用-client或-server参数)也可能需要解释。 注意用户之前的问题中提到了多线程和锁,可能需要联系JIT在优化同步机制上的作用,比如锁消除、锁粗化等优化技术,但不确定是否需要展开,根据篇幅决定是否简要提及。 要确保语言口语化,避免过于技术化的术语,但又要准确。使用分点说明,结构清晰,符合用户之前要求的层次结构。可能需要用代码示例或实际场景帮助理解,比如展示一段代码在JIT优化前后的变化。 最后总结JIT的重要性,以及在实际开发中的注意事项,比如避免过早优化,信任JVM的默认配置,但在需要时如何调整。可能还要提到GraalVM等新的JIT实现,作为扩展知识。 需要检查是否符合之前的格式要求:使用中文,行内公式用$...$,独立公式用$$...$$,虽然JIT可能涉及较少数学,但如果有性能数据可以用公式表示。确保LaTeX语法正确,结构清晰,分层次解析,对比表格等。 现在开始组织内容,确保每个部分有逻辑性,逐步深入,从概念到原理,再到应用和优化技术,最后实践建议。可能还需要对比其他编译方式,如解释执行和AOT编译,突出JIT的特点。</think>### 一、核心定义 **JIT(Just-In-Time Compiler,即时编译器)** 是Java虚拟机(JVM)的核心组件,用于**在程序运行时将热点字节码动态编译为本地机器码**,从而显著提升程序执行效率。 --- ### 二、核心价值 | 对比维度 | 解释执行 | JIT编译执行 | AOT编译(如C++) | |-----------------|----------------------------|-----------------------------|--------------------------| | 编译时机 | 逐行解释字节码 | 运行时检测热点代码后编译 | 运行前完全编译 | | 启动速度 | 快 | 中等(需预热) | 慢(需预编译) | | 长期性能 | 低 | 高(接近原生代码) | 最高 | | 内存占用 | 低 | 较高(存储编译结果) | 低 | --- ### 三、工作原理 #### 1. 执行流程 ``` .java源码 → javac编译 → .class字节码 → JVM解释执行 → JIT发现热点代码 → 编译为机器码 → 直接执行 ``` #### 2. 关键机制 - **热点检测**:通过**计数器统计**(方法调用次数/循环回边次数),默认阈值: - 方法调用:Client模式1,500次,Server模式10,000次 - 循环回边:Client模式1,000次,Server模式10,000次 - **分层编译**(Tiered Compilation): - **C1编译器**(-client):快速编译,轻度优化(如方法内联) - **C2编译器**(-server):慢速编译,激进优化(如逃逸分析) --- ### 四、经典优化技术 #### 1. 方法内联(Method Inlining) ```java // 原始代码 int add(int a, int b) { return a + b; } void calculate() { int x = add(1, 2); } // JIT优化后等效代码 void calculate() { int x = 1 + 2; } ``` **效果**:减少方法调用开销,为后续优化提供基础 #### 2. 逃逸分析(Escape Analysis) ```java // 对象未逃逸出方法作用域 void createObject() { MyObject obj = new MyObject(); // 可能被优化为栈分配或标量替换 obj.doSomething(); } ``` **效果**:可能触发**栈上分配**或**锁消除** #### 3. 循环展开(Loop Unrolling) ```java // 原始循环 for (int i=0; i<1000; i++) { sum += i; } // 优化后等效代码(假设展开因子为4) for (int i=0; i<1000; i+=4) { sum += i + (i+1) + (i+2) + (i+3); } ``` **效果**:减少循环控制指令的开销 --- ### 五、实践应用 #### 1. 性能监控 ```bash # 查看JIT编译日志 java -XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions MyApp # 输出示例 123 1 java.lang.String::hashCode (55 bytes) 245 2 sun.nio.cs.UTF_8$Encoder::encode (359 bytes) ``` #### 2. 参数调优 ```bash # 禁用分层编译(仅用C2) -XX:-TieredCompilation # 设置方法编译阈值 -XX:CompileThreshold=10000 # 禁用特定优化 -XX:-DoEscapeAnalysis ``` --- ### 六、扩展思考 #### JIT的挑战与趋势 1. **预热问题**:Serverless场景下需要快速达到峰值性能(解决方案:保存编译结果的**CDS**) 2. **GraalVM革新**:支持多语言、可替代传统C2编译器 3. **AOTJIT融合**:如JDK9引入的**AOT编译实验功能** --- ### 七、最佳实践 1. **避免过早优化**:优先保证代码可读性,信任JVM的默认配置 2. **关注稳定状态**:性能测试应在JIT预热完成后进行 3. **慎用反射**:高频反射调用会阻碍JIT优化 4. **控制代码体积**:过大的方法体会导致无法内联(默认阈值35字节) --- 通过JIT的智能编译,Java实现了**“一次编写,处处高效”**的平衡,这正是Java在性能敏感领域(如大数据处理、高频交易系统)保持竞争力的关键!
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值