Skip to content

JVM 查漏补缺 ​

本篇收录几个高频但容易被漏掉的 JVM 问题:safepoint、JIT 分层编译、逃逸分析与标量替换、锁升级。各自独立成节,随查随补。

应用safepoint时间指的是什么 ​

在Java虚拟机(JVM)中,"safepoint"(安全点)是一种机制。它确保所有线程在执行某些特定操作之前达到一个已知的执行点。JVM的垃圾回收器和其他系统操作通常需要在所有线程都到达安全点时才能执行。如线程栈的收缩或代码重优化。这样才能确保一致性和数据的完整性。

这些操作往往需要访问和修改对象和类结构这类共享数据。因此需要在没有线程正在执行Java字节码时执行它们。

"应用safepoint时间"指的是JVM中与安全点同步相关的时间。即JVM在执行需要所有线程都到达安全点的操作时,应用线程被挂起的总时间。这包括:

1、到达safepoint的时间:所有线程暂停执行正在执行的任务。并到达一个安全点所需的时间。

2、停在safepoint的时间:一旦所有线程都到达了安全点,JVM将执行所需的操作。比如垃圾回收。在这个操作执行的过程中,应用线程继续保持挂起状态。

在垃圾回收或其他需要所有线程到达安全点的场景下,应用线程会停止执行用户代码。这导致了应用停顿。这些停顿对于实时性要求高的应用来说可能是个问题。因为它们影响了应用的响应时间和吞吐量。

优化应用safepoint时间的策略包括:

  • 使用低停顿时间的垃圾回收器,如G1或ZGC。
  • 减少需要全局safepoint的操作(如减少完全垃圾回收的频率)。
  • 优化代码以减少到达安全点的时间,例如避免长时间执行的循环结构。

理解和优化safepoint相关的停顿是提高Java应用性能和实时性的重要方面。通过监控和调整JVM和应用代码,可以减少因safepoint操作而导致的应用停顿时间。

JIT 分层编译 ​

Java 是"解释执行 + JIT 编译"的混合模式。解释器立即执行但慢,JIT 把热点代码编译成本地机器码但编译有成本。分层编译(Tiered Compilation,JDK8 起默认启用)把两者组合成流水线:

层级0  解释执行,同时插桩收集 profiling
         │ 热度不足,一直留在这里
         ▼ 方法变热
层级1  C1 编译,不带 profiling(编译快,代码质量一般)
层级2  C1 编译,带基础计数 profiling
层级3  C1 编译,带完整 profiling(调用计数、分支跳转、类型分布)
         │ 更热(阈值约为万级调用)
         ▼
层级4  C2 编译,用层级3攒的 profiling 做激进优化(代码质量最高)
  • C1(Client)编译快、优化保守;C2(Server)编译慢、优化激进(深度内联、基于类型推测的去虚化、循环展开)。分层编译让代码先享受 C1 的"温饱",热到极致再换 C2 的"精致";

  • 热点探测:方法调用计数器 + 回边计数器(循环)。触发的是"方法编译"与"OSR(栈上替换,让正在跑的循环也能换编译版本)"两种入口;

  • 退优化(deopt):C2 的投机优化猜错(实际类型和 profiling 不符)会退回解释执行。这是"预热后性能突然抖动"的常见原因,压测口径见 QPS 篇的预热说明;

  • 相关调优:-XX:CompileThreshold(触发阈值)、-Xint(纯解释)、-Xcomp(强制编译,通常反而更慢)。

逃逸分析与标量替换 ​

C2 的一项核心优化,先判断一件事。判断对象是否"逃出"当前方法/线程(被 return、赋给静态字段、传给未知代码)。不会逃的对象,JVM 就不必真的在堆上造它:

  • 标量替换(Scalar Replacement):把不逃逸的对象拆散,字段直接变成局部变量放在栈上——对象根本没创建。HotSpot 的"栈上分配"实际就是靠标量替换实现的(并没有真正的栈上分配指令);

  • 锁消除(Lock Elimination):不逃逸对象上的 synchronized 没有竞争可言,直接删除——方法内 new 的 StringBuffer,其同步开销被整段抹平;

  • 锁粗化(Lock Coagulation):相邻的重复加锁解锁合并成一次,减少开销;

逃逸分析(分析,本身不产生优化)
   ├── 标量替换  → 对象不创建,字段进栈(省分配 + 减 GC)
   ├── 锁消除    → 无竞争锁直接删
   └── ( HotSpot 未做真正的栈上分配,靠标量替换等效实现)
  • 代价:逃逸分析本身要花编译时间,所以只对 C2 编译的热点代码做;参数 -XX:+DoEscapeAnalysis 默认启用;

  • 面试常见误区:"Java 对象可以在栈上分配"。严谨说法是通过标量替换达到等效效果,详见 补充-逃逸分析。

偏向锁 → 轻量级锁 → 重量级锁的升级 ​

JDK1.6 对 synchronized 做的核心优化。锁信息记录在对象头的 Mark Word 里,随竞争加剧逐级升级(只升不降):

级别Mark Word 标志适用场景获取方式成本
偏向锁101始终只有一个线程进入同步块首次 CAS 写入线程 id,之后同线程进入只比对 id,近乎无成本最低
轻量级锁00线程交替进入,竞争不激烈栈上建 Lock Record,CAS 把 Mark Word 指向它;竞争则自旋自旋烧 CPU,但不阻塞
重量级锁10多线程同时竞争除 Mark Word 外,膨胀为 ObjectMonitor,依赖操作系统 mutex,未抢到的线程阻塞挂起上下文切换,最高
只有一个线程反复拿锁            有第二个线程来竞争            自旋超过阈值/竞争持续
┌────────┐  升级   ┌────────┐   升级    ┌────────┐
│ 偏向锁  │ ─────► │ 轻量级锁 │ ──────► │ 重量级锁 │  (只升不降)
└────────┘        └────────┘          └────────┘
  • 自旋的权衡:短暂竞争里自旋等一等比挂起/唤醒便宜。但自旋占着 CPU,竞争久了必须膨胀成重量级让出 CPU。自适应自旋会根据"这把锁上次自旋有没有成功"动态调整次数;

  • 偏向锁已在 JDK15 被废弃(JEP 374)并逐步移除:同一资源被不同线程反复"偏置"时撤销偏向需要 safepoint。维护成本高、收益现代场景变小——回答时补这句是加分项;

  • 另外注意锁升级只针对 synchronized 的互斥语义,与 volatile 的可见性语义正交,对比见 volatile、synchronized 与 锁的介绍。

相关阅读 ​

2026-10 扩充(Round1 查漏)。