JVMNotes

第 07 章:解释器与 JIT

zjc 于 2026-01-07 发布

这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 Java 既有解释执行的启动优势,也有 JIT 编译到本地代码后的峰值性能。分层编译、热点探测、去优化和代码缓存共同决定了 Java 服务的预热、抖动和压测结果。

7.1 执行方式

Bytecode
  -> Interpreter
  -> C1 编译代码
  -> C2 编译代码
方式 特点
Interpreter 启动快,无需等待编译,峰值慢
C1 编译快,优化相对保守
C2 编译慢,优化激进,峰值高
Graal 可选编译器,架构不同

HotSpot 常见默认是分层编译:

java -XX:+PrintFlagsFinal -version | grep TieredCompilation

7.2 热点探测

方法触发编译的常见依据:

  1. 方法调用计数;
  2. 回边计数;
  3. 分层阈值。

相关参数:

-XX:CompileThreshold
-XX:-TieredCompilation
-XX:+PrintCompilation

查看:

jstat -printcompilation <pid> 1000
jcmd <pid> Compiler.codecache

这就是 Java 服务需要预热的原因:

启动初期
  解释执行 + C1
    -> 热点累积
       -> C2 编译
          -> 峰值性能稳定

7.3 JIT 优化

优化 说明
方法内联 把被调方法复制进调用点,减少调用开销
逃逸分析 判断对象是否逃逸
锁消除 去掉不可能竞争的锁
锁粗化 合并相邻锁操作
公共子表达式消除 避免重复计算
循环展开 减少循环控制开销
去虚拟化 根据类型 profile 内联虚方法

优化基于运行时 profile,因此同一段代码在不同流量和输入分布下性能可能不同。

7.4 去优化

JIT 根据 profile 做乐观优化。假设被打破时会去优化,回退到较低层执行。

常见触发:

  1. 新类型出现导致去虚拟化失败;
  2. 类加载改变继承关系;
  3. 异常路径进入;
  4. 优化假设失效;
  5. 监控点失效。

观察:

java -XX:+PrintCompilation -jar app.jar

输出中出现 made not entrantdeoptimized 常见于去优化。诊断参数不要在高峰生产随意开启。

7.5 Code Cache

查看:

jcmd <pid> Compiler.codecache

分段:

用途
non-method 生命周期长的 JVM stub
profiled 带 profile 的 C1 代码
non-profiled C2 或优化后代码

满了可能:

  1. 停止 JIT;
  2. 退回解释执行;
  3. 延迟升高;
  4. CPU 上升。

参数:

-XX:ReservedCodeCacheSize=256m

7.6 JIT 与容器

容器中要关注:

  1. CPU limit 与编译线程数;
  2. 启动阶段 CPU 不足导致预热慢;
  3. readiness 过早放流量;
  4. 弹性扩容后新实例冷启动;
  5. CPU throttling 与长尾延迟。

JVM 会根据可用 CPU 调整编译线程和 GC 线程。容器感知能力随 JDK 版本改进,建议使用现代 LTS。

7.7 预热策略

发布后:

1. readiness 通过后再接流量
2. 少量灰度流量
3. 观察延迟和错误率
4. 逐步增加
5. 达到稳定后接全量

压测:

低流量
  -> 中流量
     -> 目标流量
        -> 峰值流量

如果启动后立即全量压测,测到的往往是预热成本和资源竞争,不是稳定容量。

7.8 基准测试

JMH 示例:

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class StringBenchmark {

    @Benchmark
    public String concat() {
        return "a" + "b" + "c";
    }
}

JMH 要点:

  1. 防止死代码消除;
  2. fork 隔离进程;
  3. 预热;
  4. 多轮测量;
  5. 报告置信区间;
  6. 记录 JDK 和参数。

7.9 火焰图

async-profiler:

./asprof -d 30 -f cpu.html <pid>

Arthas:

profiler start
profiler stop --format html

解读:

特征 可能含义
解释器栈占比高 预热或去优化
JIT 编译线程高 启动或编译压力大
某业务方法很宽 CPU 热点
GC 栈宽 GC 占用高
lock / park 高 阻塞等待

7.10 生产案例

案例一:发布后 P99 高

现象:新实例启动后前几分钟延迟高。

排查:

jstat -printcompilation <pid> 1000
cat /sys/fs/cgroup/<path>/cpu.stat

结论:实例仍预热且触发 CPU throttling。处理:

  1. 更新 readiness;
  2. 灰度流量;
  3. 提高 CPU limit;
  4. 调整扩容阈值;
  5. 评估 AppCDS 或 AOT。

案例二:CodeCache 满

jcmd <pid> Compiler.codecache

处理:

  1. 增大 Code Cache;
  2. 减少动态类生成;
  3. 升级 JDK;
  4. 修复反射生成类问题;
  5. 观察去优化。

本章小结

解释器负责快速启动,JIT 负责峰值性能,分层编译结合两者。JIT 依赖 profile,因此服务需要预热,异常输入和类型分布可能导致去优化。生产性能评估必须区分冷启动、预热完成和稳定阶段。

思考题

  1. 为什么 Java 服务启动初期性能较低?
  2. C1 和 C2 有什么差异?
  3. 什么是去优化?
  4. Code Cache 满会造成什么影响?
  5. 如何设计发布后的预热策略?