什么是 AOT 编译?和 JIT 有什么区别?
一则或许对你有用的小广告
欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书
《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于
Spring AI + Spring Boot 3.x + JDK 21...,查看介绍《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/
新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍
截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观
面试考察点
-
编译体系认知:面试官不仅仅是想知道 AOT 和 JIT 的字面区别,更是想看你能否把 Java 从源码到执行的整个编译链路串起来——
javac编译、JIT 编译、AOT 编译分别在什么阶段、解决什么问题。 -
性能优化意识:考察你是否理解 JIT 的热点编译机制、AOT 的启动加速优势,以及它们各自的权衡取舍。
-
技术视野:如果你能提到 GraalVM、Spring Native(现 Spring AOT)这些前沿技术,说明你不仅懂传统 JVM,还关注生态演进,这在面试中是加分项。
核心答案
先说结论:
- AOT(Ahead-Of-Time,提前编译):在程序运行之前,就把字节码(或源码)编译成目标机器的本地机器码。编译发生在构建阶段。
- JIT(Just-In-Time,即时编译):在程序运行过程中,JVM 监控到某段代码被频繁执行("热点代码"),再把它动态编译成机器码。编译发生在运行时。
一张表看懂核心区别:
| 维度 | JIT 编译 | AOT 编译 |
|---|---|---|
| 编译时机 | 运行时(Runtime) | 构建时(Build Time) |
| 编译基础 | 运行时的性能 profiling 数据 | 无运行时信息,静态编译 |
| 启动速度 | 慢(需要预热) | 快(直接执行机器码) |
| 峰值性能 | 通常更高(能根据实际运行情况做激进优化) | 略低(缺乏运行时信息) |
| 内存占用 | 较高(需要维护编译线程、CodeCache 等) | 较低 |
| 跨平台 | 一次编译,到处运行(字节码) | 需要针对不同平台分别编译 |
| 代表技术 | HotSpot C1/C2 编译器、Graal JIT | GraalVM Native Image、JaegerTracing |
| 适用场景 | 长期运行的服务端应用 | Serverless、CLI 工具、快速启动场景 |
深度解析
一、Java 程序的完整编译链路
要真正理解 AOT 和 JIT,得先搞清楚 Java 代码从编写到执行的完整链路:
上图展示了 Java 代码从源文件到最终执行的完整路径。整体分为以下几个阶段:
- 前端编译:
javac将.java源码编译成.class字节码。这一步是固定的,跟 JIT 和 AOT 无关。 - 运行阶段有三种执行方式:
- 解释执行:JVM 逐条将字节码翻译成机器码执行,速度最慢,但启动最快。
- JIT 编译:JVM 在运行时发现热点代码后,将其编译成机器码缓存起来,后续直接执行机器码。
- AOT 编译:在构建阶段就把字节码直接编译成机器码,跳过了解释和 JIT 的过程。
关键点在于,JIT 和 AOT 本质上都是为了把字节码变成机器码来提升性能,区别在于什么时候做这件事。
二、JIT 编译的核心机制
JIT 是 HotSpot JVM 的王牌,它的工作逻辑可以用一句话概括:先解释执行,发现热点再编译优化。
上图展示了 HotSpot 的分层编译机制,核心流程如下:
- 第 0 层(解释执行):所有方法一开始都是解释执行,同时 JVM 会通过方法调用计数器和回边计数器来统计方法的执行频率。
- 第 1 层(C1 编译):当某个方法的调用次数超过阈值(默认约 10000 次),触发 C1 编译器进行简单优化。C1 编译速度快,但优化程度有限。
- 第 2/3 层(C2 编译):如果代码持续频繁执行,C2 编译器会介入,基于运行时 profiling 数据做更激进的优化,比如内联、逃逸分析、循环优化等。
JIT 的核心优势在于它能利用运行时的真实数据做优化。比如它发现某个虚方法在实际运行中只有一个实现,就会做去虚化(Devirtualization),直接内联,这比静态编译的 AOT 在理论峰值性能上更有优势。
但 JIT 也有明显的代价:预热(Warmup)问题。应用刚启动时所有代码都是解释执行,性能差,需要跑一段时间才能达到峰值。这也是为什么 Java 服务端应用通常要预热后才放开流量。
三、AOT 编译的核心机制
AOT 的思路很直接:别等到运行时了,构建的时候就把优化做了。
在 Java 生态中,最有代表性的 AOT 方案是 GraalVM Native Image:
# 使用 GraalVM 将 Java 应用编译为本地可执行文件
native-image -jar my-app.jar my-app
# 编译产物直接就是一个原生二进制文件
./my-app # 毫秒级启动,无需 JVM
AOT 编译的过程:
上图展示了 GraalVM AOT 编译的核心流程:
- 静态分析阶段:GraalVM 从应用的入口点(
main方法)开始,通过可达性分析(Reachability Analysis)找出所有会被用到的类和方法。没有被引用到的代码直接被剔除——这叫 "死代码消除",所以 AOT 编译出来的产物通常更小。 - 编译优化阶段:对保留下来的代码做内联、逃逸分析、常量折叠等经典编译优化。注意,这里的优化是静态的,没有运行时的 profiling 数据可用。
- 产物生成:最终输出一个平台相关的原生二进制文件,启动时不需要 JVM,直接由操作系统加载执行。
AOT 编译有一个很大的限制:反射、动态代理、JNI 等动态特性处理起来很麻烦。因为 AOT 在编译阶段就要确定所有会被用到的类,而反射是运行时才知道具体类型的。GraalVM 通过 配置文件(如 reflect-config.json)来解决这个问题,但也意味着你得手动或者通过插件来生成这些配置。
四、实战场景对比
聊到这,你可能会问:那实际开发中怎么选?
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 传统服务端应用(Spring Boot 长驻服务) | JIT | 长期运行,JIT 预热后峰值性能更高 |
| Serverless / 函数计算 | AOT | 按次计费,启动速度就是钱 |
| CLI 工具 | AOT | 没人愿意等 3 秒 JVM 启动来执行一个 0.1 秒的任务 |
| 微服务(云原生) | AOT | 容器快速弹性扩缩容,启动慢是硬伤 |
| 高性能计算、低延迟交易 | JIT | 需要运行时优化,极致峰值性能 |
Spring 6 / Spring Boot 3 引入了 Spring AOT(不是 GraalVM Native Image 的 AOT,而是 Spring 自己的构建时处理),它会在构建阶段提前完成 bean 的注册、属性注入等操作,把运行时要做的事情前移到构建时,配合 GraalVM Native Image 可以实现毫秒级启动。
面试高频追问
-
JIT 编译的 "热点代码" 是怎么判断的?
HotSpot 使用 方法调用计数器 和 回边计数器(循环回边)来统计代码的执行频率。当计数超过阈值(
-XX:CompileThreshold,默认 10000),触发 JIT 编译。还有个机制叫 "热度衰减"(-XX:-UseCounterDecay),如果一段时间没被调用,计数器会衰减,避免已经冷了的代码占用 CodeCache。 -
AOT 编译能完全替代 JIT 吗?
目前不能。AOT 缺乏运行时 profiling 数据,无法做基于真实执行的激进优化(如基于类型 profile 的去虚化、分支频率预测等)。不过差距在缩小,GraalVM 的 PGO(Profile-Guided Optimization)可以先用 JIT 跑一遍收集 profiling 数据,再用于 AOT 编译,缩小性能差距。
-
GraalVM Native Image 有什么限制?
最大的限制是对动态特性的支持:反射、动态代理、JNI、动态类加载等都需要通过配置文件显式声明。另外,所有线程必须在构建时可知,不能运行时动态创建某些类型的线程。
常见面试变体
- "Java 是解释执行还是编译执行?"
- "为什么 Java 应用启动慢?有什么优化方案?"
- "GraalVM 和传统 HotSpot 有什么区别?"
- "Spring Boot 3 的启动速度为什么能提升这么多?"
记忆口诀
JIT 边跑边优化,峰值性能顶呱呱;AOT 提前全搞定,启动飞快跨平台差。
一句话:JIT 赢在峰值,AOT 赢在启动。选谁取决于你的场景是 "跑得久" 还是 "起得快"。
