JVM 中的即时编译器(JIT)如何工作?
JIT(Just-In-Time Compiler,即时编译器)是 JVM 中 “将字节码转换为本地机器码” 的核心组件,目的是提升 Java 程序的执行效率(字节码需通过解释器逐行执行,效率低;本地机器码可直接被 CPU 执行,效率高),工作流程如下:
# 1. JIT 的触发条件:热点代码识别
JVM 通过 “热点探测” 识别 “热点代码”(被频繁执行的代码),仅对热点代码触发 JIT 编译,避免资源浪费,热点探测的核心指标是 “方法调用次数” 和 “循环执行次数”:
- (1)基于计数器的热点探测(HotSpot JVM 采用):
- 每个方法对应两个计数器:
方法调用计数器(记录方法被调用的次数)和回边计数器(记录方法中循环的执行次数,“回边” 指循环跳转指令); - 当
方法调用计数器超过-XX:CompileThreshold(默认 10000 次,C1 编译器)或回边计数器超过阈值时,JVM 标记该方法为 “热点代码”,触发 JIT 编译。
- 每个方法对应两个计数器:
# 2. JIT 的编译流程
JIT 编译分为 “前端编译” 和 “后端编译”,最终生成优化后的本地机器码:
- (1)前端编译:
- 输入:热点代码的字节码;
- 步骤:
- 词法分析:将字节码指令拆分为 Token(如操作码、操作数);
- 语法分析:将 Token 组合为抽象语法树(AST),描述代码的逻辑结构;
- 语义分析:检查语法树的合法性(如类型匹配、变量未定义),并进行初步优化(如常量折叠,
1+2优化为3)。
- (2)后端编译:
- 步骤:
- 中间代码生成:将抽象语法树转换为中间表示(IR,如 HotSpot 的 HIR、LIR),便于跨平台优化;
- 优化阶段:对中间表示进行多层优化,提升代码执行效率,常见优化手段:
- 方法内联:将被调用的小方法(如
getter/setter)直接嵌入调用者代码,减少方法调用开销; - 循环优化:循环展开(减少循环跳转次数)、循环不变量外提(将循环内不变的计算移到循环外);
- 逃逸分析:判断对象是否 “逃逸”(是否被方法外引用),若未逃逸,可进行栈上分配(避免堆内存 GC)、标量替换(将对象拆分为局部变量);
- 公共子表达式消除:若多个地方计算相同的表达式(如
a+b),仅计算一次并复用结果;
- 方法内联:将被调用的小方法(如
- 机器码生成:将优化后的中间表示转换为目标 CPU 的本地机器码(如 x86、ARM 架构),并存储在 “代码缓存”(Code Cache)中。
- 步骤:
# 3. JIT 的编译模式:C1 与 C2 编译器(分层编译)
HotSpot JVM 提供两种 JIT 编译器,采用 “分层编译”(Tiered Compilation)模式,平衡编译速度和执行效率:
- C1 编译器(Client Compiler,客户端编译器):
- 特点:编译速度快,优化程度低,适合桌面应用(如 GUI 程序);
- 适用场景:代码刚被标记为热点时,先由 C1 编译,快速生成可执行的机器码,避免程序等待;
- C2 编译器(Server Compiler,服务端编译器):
- 特点:编译速度慢,优化程度高(支持更多高级优化,如逃逸分析、向量化),适合服务器应用(如 Java Web 服务);
- 适用场景:当代码被 C1 编译后仍被频繁执行,触发 C2 编译,生成更高效的机器码,替换 C1 编译的结果;
- 分层编译流程:
- 代码初始执行时,由解释器逐行执行;
- 代码被标记为热点后,触发 C1 编译,生成基础优化的机器码;
- 代码持续被频繁执行,触发 C2 编译,生成深度优化的机器码;
- 后续执行时,直接调用代码缓存中的 C2 机器码,效率最大化。
# 4. JIT 的代码缓存(Code Cache)
- 作用:存储 JIT 编译生成的本地机器码,避免重复编译;
- 配置:通过
-XX:InitialCodeCacheSize(初始大小)和-XX:ReservedCodeCacheSize(最大大小,默认 240MB)配置; - 回收:当代码缓存满时,JVM 会清理 “不常用的机器码”(如长时间未执行的热点代码),释放空间。
上次更新: 12/30/2025