Appearance
执行引擎与 JIT
执行引擎负责把字节码真正跑起来。JVM 不是只“解释执行”,现代 HotSpot 会先解释运行,再把热点代码交给 JIT 编译器优化成机器码。理解执行引擎,可以解释启动预热、CPU 抖动、热点方法、锁优化、逃逸分析和 Code Cache 等问题。
从字节码到机器码
执行链路可以简化为:
text
Class 文件 -> 字节码 -> 解释器 -> 热点探测 -> JIT 编译 -> 机器码解释器启动快,适合冷代码;JIT 编译后的机器码运行快,适合热点代码。服务端程序常见的“刚启动慢,跑一会儿变快”,就和类加载、缓存预热、JIT 编译都有关系。
解释执行
解释器逐条读取字节码并执行。优点:
- 启动成本低。
- 不需要等编译完成。
- 适合只运行一次的冷代码。
缺点:
- 每次执行都要解释字节码。
- 热点路径性能不如机器码。
热点探测
HotSpot 会统计方法调用次数和循环回边次数,判断哪些代码值得编译优化。
常见热点:
- 高频接口方法。
- 循环体。
- 序列化 / 反序列化。
- JSON 编解码。
- 正则、加解密、压缩。
- 框架反射和代理路径。
分层编译
HotSpot 常见编译器:
| 编译器 | 特点 |
|---|---|
| C1 | 编译快,优化较轻,适合较快启动 |
| C2 | 编译慢,优化更强,适合长期运行热点 |
分层编译会让代码先由解释器执行,再进入 C1,热点足够稳定后再进入 C2。这样在启动速度和峰值性能之间折中。
常见优化
方法内联
方法调用有成本,JIT 会把小而热的方法直接展开到调用点。
好处:
- 减少调用开销。
- 暴露更多上下文,方便后续优化。
风险:
- 代码膨胀。
- 过度内联会占用 Code Cache。
逃逸分析
如果对象不会逃出当前方法或线程,JIT 可能做:
- 标量替换:把对象拆成几个局部变量。
- 栈上分配或消除分配。
- 锁消除:对象不会被其他线程访问时去掉锁。
这解释了为什么某些看似创建很多临时对象的代码,在 JIT 优化后未必真的产生同等堆压力。
锁优化
JVM 会根据竞争情况优化锁:
- 锁消除。
- 锁粗化。
- 轻量级锁。
- 重量级锁。
锁竞争严重时,仍然会表现为线程阻塞、CPU 抖动或吞吐下降。
反优化
JIT 基于运行时假设优化代码。如果假设失效,JVM 会回退到解释执行或重新编译,这叫反优化。常见触发因素:
- 类型分布变化。
- 类被动态加载。
- 内联假设失效。
- 分支行为变化。
这也是线上服务性能偶尔抖动的原因之一。
Safepoint
Safepoint 是 JVM 可以安全暂停线程的位置。很多全局操作需要线程到达 Safepoint,例如:
- GC。
- 偏向锁撤销。
- 线程栈遍历。
- 类重定义。
- JFR 或诊断采样中的部分操作。
如果线程长时间不到 Safepoint,可能影响 GC 或诊断操作的停顿时间。
Code Cache
JIT 编译后的机器码放在 Code Cache 中。它不属于 Java 堆,但占用进程内存。
常见参数:
bash
-XX:ReservedCodeCacheSize=256mCode Cache 满了可能导致 JIT 编译受限,服务性能下降。观察:
bash
jcmd <pid> Compiler.CodeHeap_Analytics
jcmd <pid> VM.native_memory summary观测工具
JFR
JFR(Java Flight Recorder)适合观察生产级性能问题:
bash
jcmd <pid> JFR.start name=profile settings=profile duration=60s filename=/tmp/app.jfr可以关注:
- CPU 热点。
- 锁竞争。
- 分配热点。
- GC 停顿。
- 方法采样。
编译日志
开发或压测环境可打开:
bash
-XX:+PrintCompilation它会输出方法编译和反优化信息。生产环境一般更推荐 JFR。
排查场景
| 现象 | 可能方向 |
|---|---|
| 刚启动性能差 | 类加载、缓存、JIT 预热 |
| CPU 高但 GC 不高 | 热点循环、锁竞争、序列化、正则 |
| 压测越跑越快 | JIT 热点编译逐步完成 |
| 某段时间性能抖动 | 反优化、Code Cache、Safepoint、系统压力 |
| 延迟尖刺 | GC、锁竞争、Safepoint、JIT 编译干扰 |
执行引擎不是日常调参的第一入口,但它能解释“为什么同一段 Java 代码运行一段时间后表现不同”。
