![图片[1]-34 Graal编译器深度解析:用Java写的Java编译器,性能真的更强吗?-速优课](http://www.suyouke.com/wp-content/uploads/2026/07/doubao_img_2304x1728_20260722_174942-1-1024x768.png)
本文导读
你可能听说过 GraalVM——这个 Oracle Labs 推出的高性能多语言执行环境近年来越来越火。但你了解它的核心基石 Graal 编译器吗?这是一个用 Java 语言编写的即时编译器,从 Java 9 开始被集成进 JDK,作为实验性的 JIT 编译器。
本文将带你深入了解 Graal 编译器:它是如何与 JVM 交互的?它和传统的 C2 编译器有什么区别?它的内部实现又是怎样的?为什么用 Java 写编译器反而可能更快?
通过阅读本文,你将了解:
- Graal 编译器与 JVM 的交互方式和 JVMCI 接口
- Graal 与 C2 编译器的核心区别与性能对比
- Graal 的编译架构:前端 IR 与后端 IR
- Graal 的激进优化策略和假设机制
- Graal 中 intrinsic 方法的实现方式
一、Graal 与 JVM 的交互
1.1 即时编译器的三大职责
我们知道,即时编译器是 JVM 中相对独立的模块,它的核心任务是把 Java 字节码编译成可以直接运行的机器码。
具体来说,即时编译器与 JVM 的交互主要体现在三个方面:
- 响应编译请求:JVM 判断某个方法足够”热”时,会向编译器发起编译请求
- 获取编译数据:编译器需要从 JVM 获取类、方法、字段等元数据,以及反映程序执行状态的 profile 数据
- 部署编译结果:编译器把生成的机器码部署到代码缓存(code cache)中
这三个功能构成了一个完整的编译周期:接收请求 → 获取数据 → 完成编译 → 部署结果。
1.2 JVMCI:解耦编译器与 JVM
传统上,即时编译器和 JVM 是紧耦合的——改编译器就得重新编译整个 JVM。这对于迭代非常活跃的 Graal 来说显然不可接受。
为了解耦,JVM 引入了 JVMCI(JVM Compiler Interface),把上面三个功能抽象成了 Java 层面的接口。这样一来,只要 JVMCI 版本不变,只需替换 Graal 相关的 jar 包(Java 9 以后是 jmod 文件)就能升级编译器,不需要动 JVM 本身。
Graal 编译器可以通过以下参数启用,它会替换掉 HotSpot 中的 C2 编译器:
-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler
1.3 JVMCI 的更多可能性
JVMCI 的作用不止是响应 JVM 发出的编译请求。实际上,Java 程序可以直接调用 Graal 来编译和部署指定的方法。
这项技术有很多应用场景:
- 单元测试:测试某个优化是否生效时,不需要反复运行方法等它变热,直接指定编译该方法即可
- Truffle 语言框架:我们下一篇要介绍的 Truffle 就是基于这项技术实现的
二、Graal vs C2:两大编译器的对决
2.1 语言之争:Java vs C++
Graal 和 C2 最直观的区别就是开发语言:
- Graal:用 Java 写的
- C2:用 C++ 写的
很多人第一反应可能是:用 C++ 写的编译器肯定更快吧?但实际上并非如此简单。
首先,当编译器充分预热后,Graal 自身的热点代码也会被即时编译成机器码,执行速度并不比静态编译的 C++ 差。
其次,就算 Graal 解释执行会慢一些,那也只是编译效率的问题,不影响编译结果的性能。换句话说,如果 C2 和 Graal 采用完全相同的优化手段,它们生成的机器码性能是一样的,程序达到稳定状态后的峰值性能也不会有差别。
用 Java 写编译器最大的优势是开发效率高、易维护。Graal 更加模块化,新功能迭代也更快。就连 C2 的作者 Cliff Click 都表示不想再用 C++ 开发 JVM 了。
2.2 优化能力的对比
由于 Java 语言更容易开发和维护,把 C2 的新优化移植到 Graal 中相对容易。但反过来就不那么容易了——比如 Graal 中的部分逃逸分析(Partial Escape Analysis)被证实非常有效,但至今没有移植到 C2 中。
另一个重要的优化分歧是方法内联算法。Graal 的内联算法对新语法、新语言更友好,比如 Java 8 的 Lambda 表达式,以及 Scala 语言。
那么实际性能对比如何呢?有人统计过数十个 Java 和 Scala 程序的峰值性能:
- Java 程序:Graal 的优势不明显,和 C2 差不多或略好
- Scala 程序:Graal 的性能优势达到了约 10%
大规模使用 Scala 的 Twitter 就在生产环境部署了 Graal 编译器,获得了约 11% 的性能提升(基于 GraalVM 社区版)。
三、Graal 的内部实现
3.1 编译架构:前端与后端
Graal 把编译过程分为前端和后端两大部分:
| 部分 | 主要职责 |
|---|---|
| 前端 | 平台无关优化(如方法内联),以及少量平台相关优化 |
| 后端 | 大部分平台相关优化(如寄存器分配),以及机器码生成 |
我们之前介绍过,Graal 和 C2 都采用了 Sea-of-Nodes IR。严格来说,这指的是 Graal 的前端 IR,也叫 HIR(High-level IR)。后端采用的是另一种非 Sea-of-Nodes 的 IR,叫 LIR(Low-level IR)。
3.2 前端:一个个优化阶段组成的流水线
Graal 的前端是由一个个独立的优化阶段(Optimization Phase)构成的。你可以把每个优化阶段想象成一个图算法:输入一张图,遍历并优化图上的节点,输出优化后的图。
前端的大部分优化阶段都可以通过配置选项开启或关闭,只有少数关键阶段是必选的。
![图片[2]-34 Graal编译器深度解析:用Java写的Java编译器,性能真的更强吗?-速优课](https://www.suyouke.com/wp-content/uploads/2026/07/image-42.png)
感兴趣的话可以去 Graal 源码里看配置这些优化阶段的文件:
HighTier.java、MidTier.java、LowTier.java。
3.3 激进的投机性优化
Graal 和 C2 都采用了激进的投机性优化(Speculative Optimization)。
这类优化基于某种假设(Assumption)。如果假设成立,优化后的代码会更快;如果假设不成立,JVM 会通过去优化(Deoptimization)机制从编译执行切换回解释执行,必要时还会废弃这份机器码,重新收集 profile 后再编译。
举个经典的例子:类层次分析(CHA)。在做虚方法内联时,如果发现某个接口只有一个实现,就可以假设之后也只有这一个实现,直接内联。如果后来加载了第二个实现,就废弃掉之前的编译结果。
Graal 比 C2 更加激进。它从设计上就非常青睐基于假设的优化:
- 支持自定义假设,直接与去优化节点关联
- 当去优化触发时,JVM 会记录是哪个假设失败了
- 第二次编译同一个方法时,Graal 就知道这个假设不成立,不会再用同样的激进优化
这种”吃一堑长一智”的机制,让 Graal 敢于做更多的激进优化,同时又能保证正确性。
3.4 Intrinsic 方法的实现
Intrinsic 方法是 JVM 中另一个重要的性能优化手段。之前我们专门有一篇文章介绍过。
在 Graal 中实现高性能的 intrinsic 方法相对简单。Graal 提供了一种方法替换机制:在解析 Java 字节码时,把匹配到的方法调用替换成对另一个内部方法的调用,或者直接替换成一个特殊节点。
举个例子,数组比较方法 java.util.Arrays.equals(byte[], byte[]) 可以被替换成一个特殊的 IR 节点,用一个节点来代表整个数组比较逻辑。这样做的好处是:
- 当前编译方法对应的图被简化了
- 更容易与其他优化(如循环优化、常量折叠)结合
总结与思考
本文要点回顾
- Graal 通过 JVMCI 与 JVM 交互:
- JVMCI 把编译器与 JVM 解耦,升级编译器无需重新编译 JVM
- 三大交互:响应编译请求、获取编译数据、部署编译结果
- Java 程序可以直接调用 Graal 编译指定方法
- Graal vs C2:
- Graal 用 Java 写,更易开发维护;C2 用 C++ 写,历史包袱重
- 峰值性能取决于优化手段,与编译器开发语言无关
- Graal 的优化更激进,部分逃逸分析等优化还没移植到 C2
- 对 Scala 等语言,Graal 的性能优势更明显(约 10%)
- Graal 的编译架构:
- 前端 HIR:Sea-of-Nodes,平台无关优化为主
- 后端 LIR:非 Sea-of-Nodes,平台相关优化和代码生成
- 前端由一个个可配置的优化阶段组成
- 激进的投机性优化:
- Graal 支持自定义假设,与去优化节点直接关联
- 去优化时记录失败的假设,二次编译时避开
- 这种机制让 Graal 敢于做更多激进优化
- Intrinsic 方法:Graal 通过方法替换机制实现 intrinsic,可以直接替换为特殊 IR 节点,便于后续优化。
思考与实践
- 你觉得用 Java 编写 JIT 编译器还有哪些优势或劣势?
- 投机性优化的核心是”假设-验证”机制,在你的工作中有没有类似的设计思路?
- 动手试试启用 Graal 编译器,对比一下你的应用在 C2 和 Graal 下的性能差异。
值得注意的是,Graal 编译器本身也需要被即时编译,所以在程序刚开始运行时,它会抢占本应用于编译应用代码的计算资源,导致启动性能较差。这个问题我们会在最后一篇介绍 SubstrateVM 时给出解决方案。








请登录后查看评论内容