跳到主要内容
约 9 分钟4 张卡

GC 停顿分析

GC 停顿分析用于判断垃圾回收怎样影响业务延迟。本文从统一日志配置入手,说明怎样读懂回收事件,再用 JFR 关联同一时刻的应用活动。JFR 是 JVM 内置的运行事件记录工具。最后,本文介绍常见原因和调优后的验证方法。

1. 面试核心​

先记住这几条,再读正文
  • 日志入口。 -Xlog:gc*,safepoint 可同时记录 GC 与安全点事件。
  • 容量解读。 GC 日志中括号内的堆容量表示当前容量,-Xmx 表示允许使用的最大堆容量。卡 · 堆容量与 CPU 时间
  • 存活占用趋势。 在负载相近、回收范围一致时,GC 后存活占用持续增长说明对象保留量在增长。卡 · Unified Logging 配置
  • 事件关联。 JFR 可将 GC、分配样本与线程事件放入同一时间线。卡 · Unified Logging 配置

2. Unified Logging 配置​

分析停顿前,需要让日志同时记录回收事件和对应时间。本文使用 Java 17、21 支持的 Unified Logging,也就是统一日志机制:

java \
-Xlog:gc*,safepoint:file=/logs/gc.log:time,uptime,level,tags:filecount=10,filesize=100m \
-jar app.jar

time 记录实际日期时间,便于对齐监控和发布事件。uptime 记录 JVM 启动后的运行时间,便于识别重启前后的事件。filecount 和 filesize 控制日志轮转,避免日志持续占满磁盘。长期运行还应配合磁盘告警,具体支持项可用 java -Xlog:help 查看。

默认信息不足以解释某次暂停时,可短期开启 gc+phases=debug,查看各阶段耗时。排查结束后恢复已验证的日志级别,减少无关调试日志的写入。

3. GC 事件信息​

一条暂停时长只能说明耗时,解释原因还需要回收范围和空间变化。读日志时,先关联以下四类信息:

  1. 收集器与事件类型:例如 G1 的 Young、Mixed、Full,或 ZGC 的回收周期。
  2. 触发原因与 GC ID:例如分配失败、元数据阈值或 System.gc()。GC ID 用于把同一次回收的多行日志关联起来。
  3. 回收前后占用与当前容量:比较腾出了多少空间,以及剩余可用空间是否充足。
  4. 暂停时间与阶段耗时:确认总暂停有多长,再找最慢的阶段。

回收后占用明显下降,随后又快速上涨,说明要重点检查分配率。回收后仍然很高,则要结合回收范围,检查存活对象是否过多。Young GC 主要处理年轻代,老年代占用高不能直接判为泄漏。

阶段耗时可以进一步缩小范围。Object Copy 慢时,重点看存活对象的复制量。Root Scan 慢时,检查线程根、根集合和 RSet,也就是记录跨区域引用的记忆集。

3.1 堆容量与 CPU 时间​

日志里的占用、容量和 CPU 时间含义不同,读错就会误判堆大小或停顿成本。下面用一组示意日志说明:

GC(12) Pause Young (Normal) (G1 Evacuation Pause) 800M->200M(1024M) 20.000ms
GC(12) User=0.06s Sys=0.01s Real=0.02s

800M 与 200M 分别是此次 GC 前后的堆占用,因此本次净减少了 600M。括号里的 1024M 是当前堆容量,最大堆容量需另查 -Xmx。末尾的 20ms 是此次暂停事件的耗时。

User 与 Sys 分别记录该区间内进程在用户态和内核态消耗的 CPU 时间。Real 是实际经过的时间,也称墙钟时间。多个 GC 线程并行工作时,CPU 时间会累加,所以 User + Sys 可以超过 Real。

Real 明显偏高而 CPU 时间较少时,应继续检查调度延迟、CPU 节流和缺页等待。CPU 时间描述处理器消耗,业务损失还要结合请求延迟与吞吐判断。

4. GC 类型与回收范围​

不同收集器对 Minor、Major 和 Full 的使用方式有差异。分析报告应写出具体收集器和日志事件名,再说明实际回收区域与结果。还要区分并发工作和 STW,即暂停应用线程执行的阶段。

例如,G1 Mixed 回收年轻代与部分老年代 Region,属于正常的局部疏散。Java 21 的分代 ZGC 有年轻代与老年代的回收周期,传统模式则不分代。分析时应确认实际启用的模式。并发周期包含与应用同时执行的工作,所以周期总时长与 STW 总时长不同。

ZGC 的分代模式见低延迟垃圾收集器。

5. GC 停顿、安全点与系统停顿​

请求延迟的长尾,也就是少数特别慢的请求,可能来自 GC 之外的等待。分析时要在同一时间窗口区分以下五类耗时:

  • JVM 等待线程到达安全点,才能开始全局操作。
  • GC 在线程暂停期间扫描或复制对象。
  • 进程受到 CPU 配额节流,或等待操作系统调度。
  • 进程发生需要读盘的缺页、内存换页,或虚拟机等待宿主机分配 CPU。
  • 应用等待锁、线程池、I/O 或下游响应。

安全点日志会区分到达安全状态的时间与停顿内的工作时间。因此,某个 GC 阶段很短时,还要检查整个事件的总时间。若 GC 与业务长尾不在同一窗口,就应继续查应用等待和系统调度。

6. JFR 关联分析​

GC 日志能定位回收阶段,JFR 则帮助判断当时应用在做什么。下面采集 5 分钟的记录,采集窗口应覆盖延迟升高的时段:

jcmd <pid> JFR.start name=latency settings=profile duration=5m filename=/logs/latency.jfr

可以用 JMC(JDK Mission Control,JFR 分析工具)打开记录。按实例和时间对齐业务监控后,重点查看以下六组事件:

  • GC pause 与 concurrent phase 分别显示暂停和并发阶段。
  • 分配事件与样本显示对象创建位置和分配压力。
  • Old Object Sample 与 heap statistics 提供存活对象和堆占用线索。
  • Safepoint、CPU Load 与线程活动帮助判断协调和调度成本。
  • Java Monitor Blocked 与 Thread Park 帮助定位锁竞争、线程等待。
  • Compilation 与 Code Cache 帮助判断即时编译、代码缓存和预热成本。

解释分配量时,需要先确认事件的记录范围。TLAB 是堆中供单个线程快速分配对象的区域。ObjectAllocationInNewTLAB 只记录触发新 TLAB 的那次分配。ObjectAllocationOutsideTLAB 则记录 TLAB 外的分配,因此两类事件的条数无法代表所有对象。

ObjectAllocationSample 用采样表示分配压力,估计分配量时需要使用样本权重。事件是否启用、阈值多高,由录制配置(recording settings)决定。看不到事件时,应先检查配置是否记录了它。

Old Object Sample 能指出部分长期存活对象,但采样只覆盖其中一部分。若要确认对象为什么一直被持有,还需结合堆转储分析保留路径。

7. 常见原因​

同样是 GC 频繁,分配量大和存活量大需要不同处理。下面按日志中的现象,说明各自的原因与下一步检查方向。

7.1 分配压力​

请求量或批次增大会推高每秒分配字节数,年轻代因此更快填满。回收变得频繁后,GC CPU 也会上升,进而影响吞吐和请求长尾。可以用分配分析定位创建位置,优先减少无用中间对象,并限制单批处理峰值。

7.2 存活对象与堆余量​

live set 是回收后仍需保留的存活对象总量。它接近堆上限时,能用于新分配和疏散的空间都会减少。并发周期随之变频繁,严重时会出现 Full GC、退化回收或分配停滞,具体取决于收集器。

这时要区分业务确实需要更多对象,还是已经无用的对象被意外持有。前者评估堆容量,后者修复对象生命周期。

7.3 安全点到达延迟​

日志中的 Reaching safepoint 较高,说明 JVM 花了较长时间协调线程进入安全状态。这段等待发生在停顿工作开始之前,会增加应用感知的延迟。需要检查对应线程状态、JIT 编译代码和系统调度,并结合目标 JDK 判断。

线程执行 native 代码时可以处于安全状态,具体见安全点。收集器暂停目标主要用于安排回收工作,调低它无法直接消除线程协调延迟。

7.4 显式回收与元数据阈值​

部分回收由显式请求或元数据压力触发,因此要先核对 cause。应用调用 System.gc() 时,G1 下可评估 -XX:+ExplicitGCInvokesConcurrent,让显式请求走并发回收路径。-XX:+DisableExplicitGC 则用于忽略应用的显式 GC 请求。

诊断工具仍可请求回收,需检查采集记录。相关影响见JVM 诊断。

Metadata GC Threshold 指向类元数据分配压力,应检查 Metaspace 占用与类加载器增长。Metaspace 是存放类元数据的本地内存区域。这个原因本身不足以证明 Java 堆泄漏。

8. 调优步骤​

调优要验证业务是否改善,并让每次变化都能对应到原因。建立基线后,按以下六个环节推进:

  1. 明确业务延迟、吞吐与资源目标。
  2. 建立日志、JFR 和负载基线。
  3. 修复异常分配与无界持有。
  4. 确认堆、容器内存和 CPU 配额有余量。
  5. 每次只改一类参数,并回放相同负载。
  6. 比较暂停分位、GC CPU、吞吐和 RSS。RSS 表示进程当前驻留在物理内存中的大小。

把收集器默认自适应配置作为比较基线,再针对已观察到的原因调整。一次加入大量参数会让收益难以归因,也会改变原有的自适应行为。

9. 常见问题​

9.1 Young GC 很频繁就是问题吗?​

高频 Young GC 也可以是正常状态。单次暂停短、GC CPU 可接受且业务目标满足时,频率高本身无需处理。判断时还要对照分配率、总 CPU 和端到端延迟。

9.2 出现一次 Full GC 就需要调堆吗?​

9.3 开启 JFR 会影响性能吗?​

10 · 面试题卡片​

10.1 线上 Full GC 频繁,怎样排查​

出现公司:阿里云

考察重点:区分分配压力、保留增长、元数据与显式回收。

相关内容:Unified Logging 配置至调优步骤。

参考回答

排查频繁 Full GC,要先用日志区分空间压力、元数据压力和显式请求。

确认收集器后,按以下四步继续:

  1. 读取每次 Full GC 的 cause、回收范围、前后占用和最慢阶段。
  2. 对齐分配率、live set、Metaspace 与 Humongous 数量,并核对显式 GC 和诊断操作。
  3. 回收后持续高时,分析持有链与业务容量。快速涨回时,用 JFR 或分配分析查创建热点和批次峰值。元数据压力则重点查类加载器增长。
  4. 先修复无界保留与异常分配,再评估堆余量和参数。每次只改一个主要变量,用同一负载比较暂停、GC CPU、吞吐和 RSS。

10.2 JFR 和 GC 日志各自能回答什么​

出现公司:阿里云

考察重点:关联回收阶段、容量变化与同时发生的运行事件。

相关内容:Unified Logging 配置至JFR 关联分析。

参考回答

GC 日志说明一次回收做了什么,JFR 帮助解释它与哪些应用活动同时发生。

日志提供回收类型、原因、容量变化与阶段耗时。JFR 则把分配样本、CPU、线程等待、安全点和编译事件放到同一时间线。

两者要按实际时间、uptime 和实例信息对齐,才能与请求监控准确关联。这样既能找到回收的主要成本,也能判断业务是否同时在等待锁或 I/O。

10.3 800M→200M(1024M) 表示什么,1024M 就是 Xmx 吗?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:区分堆占用、当前容量与最大堆容量,并结合回收范围解释变化。

相关内容:堆容量与 CPU 时间。

参考回答

800M 和 200M 分别表示回收前后占用,1024M 表示当前容量,未必等于 -Xmx。

最大堆容量需要查看启动配置。此次净减少了 600M,但还要结合 Young、Mixed 或 Full 的范围判断剩余对象。

10.4 能直接用 JFR 分配事件条数计算对象总量吗?​

出现公司:暂无已核验的公开来源(本站自测)

考察重点:解释 TLAB 分配事件覆盖哪些对象,以及分配采样为什么需要结合权重。

相关内容:JFR 关联分析。

参考回答

不能直接用事件条数计算全部对象数量,因为这些事件的记录范围不同。

NewTLAB 只记录触发新 TLAB 的那次分配,OutsideTLAB 记录 TLAB 外分配。AllocationSample 则是采样结果,估计分配量时要结合权重。最后再用分配率指标核对总量趋势。

11. 参考资料​