什么是 AOT 编译?和 JIT 有什么区别?
结论先行:**AOT(Ahead-Of-Time,提前编译)**是在程序运行之前就把字节码编译成本地机器码,产物是平台相关的可执行文件或共享库;JIT(Just-In-Time,即时编译)是在运行期间根据实际执行情况编译热点代码。两者的核心差别不在「编译」,而在有没有运行期画像:AOT 只能靠静态分析与预热构建期的 profile 猜测,JIT 能看到真实的类型分布与分支走向,因此 AOT 启动快、峰值性能通常略低。
一、对比
| 维度 | AOT | JIT |
|---|---|---|
| 编译时机 | 构建期 | 运行期(首次达到热点阈值后) |
| 产物 | 原生可执行文件 / 动态库 | 内存中的机器码缓存 |
| 启动速度 | 毫秒级,无预热 | 秒级到数十秒,需要预热 |
| 峰值性能 | 接近 JIT,缺少动态优化 | 更高,可内联、去虚化、按 CPU 生成指令 |
| 内存占用 | 低,无需 JIT 编译器与元空间代码缓存 | 高,需 CodeCache、元空间、编译线程 |
| 动态性 | 反射、动态代理、动态类加载需额外配置 | 原生支持 |
| 典型实现 | GraalVM native-image、JDK 9 的 jaotc(已移除) | HotSpot C1/C2、Graal JIT |
二、时间轴上的位置
JIT:
启动 ── 解释执行 ── 热点探测 ── C1 编译 ── C2 编译(峰值)
│ ↑ 慢 ↑ 快
└─ 冷启动阶段,QPS 爬坡
AOT:
构建期 ── 静态分析 + 可达性分析 ── 机器码 + 堆快照
启动 ────────────────────────── 直接跑(毫秒级)
组合(CDS / AppCDS / Leyden):
构建期存档类元数据 ── 启动直接映射 ── 仍由 JIT 接管热点
三、AOT 的典型用法
# GraalVM 原生镜像:构建期把可达的类全部编译进可执行文件
native-image \
--no-fallback \
-H:Name=order-service \
-H:ReflectionConfigurationFiles=reflect-config.json \
-H:IncludeResources='application.*\.yml' \
-jar order-service.jar
# 启动后没有 JVM 预热:直接测 QPS
./order-service &
time curl -s http://127.0.0.1:8080/api/order/1 > /dev/null
reflect-config.json 片段(反射需要显式声明):
[
{ "name": "com.example.Order", "allDeclaredMethods": true },
{ "name": "com.example.OrderMapper", "methods": [ { "name": "selectById" } ] }
]
四、不换运行时的「半个 AOT」:CDS
如果不想换 GraalVM,可以用**类数据共享(CDS)**把类加载与验证的结果存档,减少启动期的解析与验证开销:
# 生成基线存档(JDK 12+ 默认自带 classes.jsa)
java -Xshare:dump -XX:SharedArchiveFile=app.jsa -cp app.jar
# 应用存档启动,观察启动耗时
java -Xshare:on -XX:SharedArchiveFile=app.jsa -cp app.jar com.example.Main
# 动态存档:先跑一段,再落盘
java -XX:ArchiveClassesAtExit=app.jsa -cp app.jar com.example.Main
五、常见追问
| 追问 | 回答要点 |
|---|---|
| AOT 一定更快吗 | 启动快、内存小,但峰值通常不如 JIT,因为缺少运行期类型画像,虚方法只能保守处理 |
| JDK 9 的 jaotc 呢 | 实验特性,JDK 16 起废弃、JDK 17 移除,主流 AOT 方案是 GraalVM |
| AOT 为什么反射会失败 | 构建期静态分析看不到反射目标,类被裁掉;需要 reflect-config、resource-config 显式声明 |
| 什么服务适合 AOT | Serverless、CLI、冷启动敏感的边缘服务;长驻的高吞吐服务用 JIT 更划算 |
| AOT 与 JIT 会长期并存吗 | 会,Project Leyden 的方向是「静态镜像 + 运行期优化」结合,让两者优势叠加 |
小结
AOT 把编译成本搬到构建期,换来快启动小内存;JIT 把编译留给运行期,换来更强的优化上限。选型看「冷启动是否在关键路径」和「能否接受构建期配置反射」,而不是谁的编译技术更先进。