Dmitry Chuyko 发布于 2024年5月14日
Java 应用程序默认采用即时编译(Just-in-Time,JIT)。然而,GraalVM Native Image 使得对 Java 程序进行提前编译(Ahead-of-Time,AOT)成为可能。
这两种方法有何区别?是继续沿用久经考验的 JIT 编译,还是勇敢迈入 AOT 的新世界?本文将为你揭晓答案!
目录
- Java 中的编译原理
- 什么是 JIT 编译器
- 支持 CRaC 的 Alpaquita 容器:更快启动、更小占用
- HotSpot 与 GraalVM 对比
- 什么是 AOT 编译器
- Liberica Native Image Kit:增强性能与调试能力
- JIT 与 AOT:总结
Java 中的编译原理
程序编译是指将用高级编程语言(此处为 Java)编写的程序翻译成计算机可理解的底层机器码的过程。生成的机器码包含二进制指令,可由 CPU 直接执行。这一过程由编译器完成——它以最高效的方式将源代码转换为机器码,并可执行多种优化,以提升程序运行速度或降低资源消耗。
由于 Java 是一种平台无关的语言,其源代码首先会被编译为 JVM 字节码(bytecode),然后由目标计算机上安装的 JVM 编译器将其解释为平台相关的机器码。这一额外步骤实现了 Java “一次编写,到处运行”(Write Once, Run Anywhere, WORA)的核心理念。
源代码可以在程序执行前或执行过程中被翻译为机器码,这正是 JIT 与 AOT 发挥作用的地方。
什么是 JIT 编译器
在即时编译(Just-in-Time compilation,也称动态编译)中,JVM 字节码由 JVM 在程序启动后(即运行时)转换为机器码。
JIT 编译器的目标是生成高性能代码。首先,它会分析代码,判断哪些部分使用频率更高(即“热点”)、运行在哪个操作系统上等;其次,它会应用各种优化策略,如全局优化、局部优化、控制流优化等。通常,优化越充分,生成的代码性能越好,但“预热时间”(warm-up time)——即实际程序执行前的延迟——也会越长。
优化强度因程序而异。最流行的 JVM HotSpot 提供了两个编译器:
- C1(客户端编译器):立即开始优化,执行简单优化。因此启动更快,但生成的代码优化程度较低。
- C2(服务端编译器):先观察运行中的代码一段时间,收集性能分析数据,再开始优化。虽然预热时间更长,但生成的代码性能优于 C1。
此外,JIT 编译器还能进行自适应优化,在应用程序运行期间动态重新编译代码片段(例如,突然调用之前未使用的方法)。
JIT 编译的优势在于:
- 为特定用例和运行环境生成高度优化的代码;
- 支持丰富的垃圾回收器(GC),包括新版 Java 中的低延迟 GC(如 ZGC),从而根据需求降低延迟或提高吞吐量。
但 JIT 也有明显缺点:
- 预热阶段可能持续数分钟,在此期间应用处理请求的能力低于稳定状态,导致初期延迟较高。更糟的是,每次重启应用都会重复这一过程。
- 预热期间资源消耗更高,因此你不得不为实例分配比实际需要更多的内存。
- 容器镜像体积较大,因为其中包含了 JVM 和编译后的代码。
| JIT 优势 | JIT 劣势 |
|---|---|
| 多种优化技术带来高性能代码 | 预热时间长 |
| 运行时动态性能优化 | 启动初期开销高 |
| 成熟的调试与监控工具 | 容器镜像包含 JVM,体积大 |
支持 CRaC 的 Alpaquita 容器:更快启动、更小占用
想象一下:如果能预热应用、保存其状态到文件,然后像游戏存档一样从暂停点瞬间恢复,会怎样?
好消息是,借助支持 Coordinated Restore at Checkpoint (CRaC) API 的 Alpaquita 容器,你可以做到这一点。CRaC 允许开发者对正在运行的 Java 应用创建快照,将快照复制到多个云实例,并从中断处瞬间恢复应用至峰值性能状态。这样,启动和预热时间从几分钟缩短到毫秒级,且无内存开销(因为应用状态已稳定)。
此外,基于极简 Alpine 风格的 Alpaquita Linux 构建的轻量级容器,进一步减小了整体镜像体积。

Spring Boot Petclinic 与 Alpaquita 容器 + CRaC:启动性能研究结果
关键在于,JIT 编译器依然存在,因此仍可进行动态性能优化。
Alpaquita 容器(基于 Alpaquita Linux 和 Liberica JDK)开箱即用地支持 CRaC API,无需手动调整 JDK 或操作系统。
HotSpot 与 GraalVM 对比
需要指出的是,HotSpot 并非 Java 生态中唯一的 JIT 编译器。IBM 开发的 OpenJ9 也是选项之一(可参考 HotSpot 与 OpenJ9 性能对比)。但与用 C/C++ 编写的 HotSpot 和 OpenJ9 不同,GraalVM 是一个用 Java 编写的 JDK。
相比 HotSpot 的 JIT 编译,GraalVM 的 JIT 编译器提供了更多优化,在某些场景下 性能优于 HotSpot。此外,GraalVM 还提供 Truffle 框架,可高效运行非 JVM 语言,适用于多语言项目。
但最引人注目的是,GraalVM 还包含 Native Image 技术,支持对 Java 应用进行提前编译(AOT),这也引出了本文的核心话题。
什么是 AOT 编译器
提前编译(Ahead-of-Time,AOT)发生在程序执行之前,即构建阶段。GraalVM 的 AOT 编译器在“封闭世界假设”(closed-world assumption)下对代码进行静态分析——即假设所有运行时所需的类在构建时都已可达(该假设的潜在问题将在下文讨论)。编译器将字节码翻译为特定操作系统和架构的机器码,所有必要类在构建时初始化,其代码与 JDK 中所需的库类和静态链接代码一起打包成单一原生可执行文件。
由于无需在运行时查找热点或解释字节码,原生镜像几乎可以瞬间启动。
AOT 编译的优势包括:
- 大幅降低内存消耗(移除未使用代码和依赖,且无需 JVM);
- 启动速度提升至 1/10 秒级;
- 无需预热即可达到峰值性能;
- 攻击面更小,安全性更高;
- 反向工程难度极高(尤其配合混淆器使用),为知识产权提供额外保护。
尽管迁移到 Native Image 听起来极具吸引力,但该技术也存在明显缺点和迁移挑战:
- 构建原生镜像极其耗费资源,通常需分配数 GB 内存;
- 生成的可执行文件不含 JVM,无法进行后续性能优化;
- 垃圾回收器选择有限:GraalVM 社区版仅支持 SerialGC,企业版支持 G1GC;
- 不支持 Java 的动态特性:如反射(Reflection)、JNI、动态代理(Dynamic Proxy)等。迁移现有项目时,必须通过重写代码或使用 Tracing Agent 生成 JSON 元数据文件,告知 AOT 编译器所需动态内容。
| AOT 优势 | AOT 劣势 |
|---|---|
| 几乎瞬时启动 | 无法动态优化性能 |
| 镜像体积小 | 不兼容 Java 动态特性 |
| 攻击面小 | 垃圾回收器选择有限 |
| 难以反编译 | 诊断调试困难 |
Liberica Native Image Kit:增强性能与调试能力
为提升原生镜像的整体性能,BellSoft 推出了基于 GraalVM CE 的 Liberica Native Image Kit (NIK)。
我们为其新增了 ParallelGC 实现,据研究可将原生镜像的延迟降低 10–40%。
macOS 用户现在也能方便地调试原生镜像,该功能已集成至 Liberica NIK。
Liberica NIK 已被 Cloud Native Buildpacks 默认采用,并被 Spring 官方推荐 作为原生构建工具,确保与 Spring Boot 项目的无缝集成。
JIT 与 AOT:总结
综上所述,JIT 与 AOT 编译器为优化 Java 应用性能提供了不同路径:
- JIT 编译:提供更高的整体性能和运行时动态优化能力,但预热时间长、内存开销大,不适合某些云部署场景。可通过支持 CRaC 的 Alpaquita 容器缓解这些问题。
- AOT 编译:生成几乎瞬时启动的原生镜像,体积小、内存占用低、安全性高。但缺乏动态优化能力、GC 选择少,且迁移复杂(受限于封闭世界假设)。使用 Liberica NIK 可通过 ParallelGC 改善 GC 性能。
没有放之四海而皆准的方案。JIT 与 AOT 各有适用场景。要选择最适合你项目的方案,应结合你的服务等级目标(SLO)权衡两者优劣,并评估相关风险。