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 事件信息
一条暂停时长只能说明耗时,解释原因还需要回收范围和空间变化。读日志时,先关联以下四类信息:
- 收集器与事件类型:例如 G1 的 Young、Mixed、Full,或 ZGC 的回收周期。
- 触发原因与 GC ID:例如分配失败、元数据阈值或
System.gc()。GC ID 用于把同一次回收的多行日志关联起来。 - 回收前后占用与当前容量:比较腾出了多少空间,以及剩余可用空间是否充足。
- 暂停时间与阶段耗时:确认总暂停有多长,再找最慢的阶段。
回收后占用明显下降,随后又快速上涨,说明要重点检查分配率。回收后仍然很高,则要结合回收范围,检查存活对象是否过多。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. 调优步骤
调优要验证业务是否改善,并让每次变化都能对应到原因。建立基线后,按以下六个环节推进:
- 明确业务延迟、吞吐与资源目标。
- 建立日志、JFR 和负载基线。
- 修复异常分配与无界持有。
- 确认堆、容器内存和 CPU 配额有余量。
- 每次只改一类参数,并回放相同负载。
- 比较暂停分位、GC CPU、吞吐和 RSS。RSS 表示进程当前驻留在物理内存中的大小。
把收集器默认自适应配置作为比较基线,再针对已观察到的原因调整。一次加入大量参数会让收益难以归因,也会改变原有的自适应行为。
9. 常见问题
9.1 Young GC 很频繁就是问题吗?
高频 Young GC 也可以是正常状态。单次暂停短、GC CPU 可接受且业务目标满足时,频率高本身无需处理。判断时还要对照分配率、总 CPU 和端到端延迟。
9.2 出现一次 Full GC 就需要调堆吗?
要先确认触发原因。空间不足需要分析容量和存活量,显式 GC、诊断或维护操作则要检查发起者。长期在线服务应尽量减少不可预测的长停顿,判断时同时记录耗时与重复频率。
9.3 开启 JFR 会影响性能吗?
JFR 会增加记录开销,但其设计支持低开销持续采集。实际成本取决于事件类型、触发阈值和堆栈深度。default 适合持续记录,profile 适合短时分析,修改配置后应在目标负载测量。
10 · 面试题卡片
10.1 线上 Full GC 频繁,怎样排查
出现公司:阿里云
考察重点:区分分配压力、保留增长、元数据与显式回收。
相关内容:Unified Logging 配置至调优步骤。
参考回答
排查频繁 Full GC,要先用日志区分空间压力、元数据压力和显式请求。
确认收集器后,按以下四步继续:
- 读取每次 Full GC 的 cause、回收范围、前后占用和最慢阶段。
- 对齐分配率、live set、Metaspace 与 Humongous 数量,并核对显式 GC 和诊断操作。
- 回收后持续高时,分析持有链与业务容量。快速涨回时,用 JFR 或分配分析查创建热点和批次峰值。元数据压力则重点查类加载器增长。
- 先修复无界保留与异常分配,再评估堆余量和参数。每次只改一个主要变量,用同一负载比较暂停、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 则是采样结果,估计分配量时要结合权重。最后再用分配率指标核对总量趋势。