最后更新:2026-11-02
适用环境:HotSpot JDK 17 或 21,以 G1 为例;Linux、Bash,已安装对应 JDK 的诊断工具

GC 次数一直涨,接口也开始变慢,两件事可能有关,但还不能直接得出“GC 把服务卡住了”。频繁而短的回收、少数很长的暂停,以及数据库或线程等待,处理方法并不一样。先把慢请求和 JVM 事件对到同一时间窗口,再决定改代码、内存预算还是运行环境。

本文先使用已有日志与短时采样,再补充有界的 JFR 录制。示例 PID、日志目录和文件名都要换成实际值;没有给出任何生产实测输出或性能改善比例。

先对齐一段慢请求与 GC 时间

记下异常实例、发布版本、请求时间、端到端耗时和当时的负载。多副本环境必须找到真正处理慢请求的实例,不能拿另一台机器的 GC 曲线来解释。

看到的现象需要补齐的证据
GC 次数增长很快,接口延迟基本稳定单次暂停、暂停累计时间、分配速率与当前负载
多个请求在相近时刻一起变慢同一时刻的 GC 暂停、safepoint 与 CPU 调度情况
堆使用率回落后又快速上涨分配热点、存活对象与缓存、批任务的变化
接口很慢,却没有对应的 GC 暂停日志是否覆盖该时段,再查线程、数据库连接和外部调用等待

应用日志通常记录墙上时间,部分 JVM 日志还带进程运行时长。先核对时区、时间精度和进程启动时刻。进程重启后,PID、累计计数和运行时长都会变化,跨重启直接相减会得到错误结论。

若主要现象是 CPU 持续打满,可同时对照 Java 进程 CPU 100% 怎么定位;若已经出现内存溢出或容器被杀,先按 Java OOM 排查 保留对应现场。

确认正在运行的 JVM 和收集器

在目标 JVM 所在环境,以启动应用的用户和相应组身份执行。下文命令沿用同一 Bash 会话里的 APP_PID:

APP_PID=12345

jcmd "$APP_PID" VM.version
jcmd "$APP_PID" VM.flags

确认实际 JDK、堆上限和启用的收集器,不以终端单独执行 java -version 的结果代替目标进程版本。本文后续按 G1 日志解释;若实际使用 ZGC、Parallel GC 等,需改看对应版本文档,不能硬套字段和阶段。

jcmd 要求在 JVM 所在机器及相应用户身份下使用。容器还要对齐 PID 命名空间、工具和文件系统视图;宿主机看到的 PID、容器内 PID 与日志路径可能不同。连接失败时先查身份、目标进程和诊断接口限制,不把“工具没有连上”解释成“没有 GC 问题”。JDK 21 jcmd 文档及 JDK 17 对应文档列出了命令要求。

用短采样判断变化速度,不拿累计值当停顿

先采一小段 GC 概况,避免一开始就生成大体积堆转储:

jstat -gcutil -t "$APP_PID" 1000 10

这里间隔为 1000 毫秒,采样 10 次;-t 增加目标 JVM 的运行时长。重点看同一进程、同一窗口内的变化:

  • YGC、FGC 是对应回收事件的累计次数,观察它们在窗口内增加多少。
  • YGCT、FGCT 是对应累计回收时间。它们不是某次请求的延迟,也不给出暂停分布。
  • E、O 等是相应空间相对当前容量的使用率,不是整个进程占用的物理内存。

YGC 增长快,并不自动等于异常。还要看每次暂停是否影响目标延迟、单位业务负载下的分配量是否变化,以及回收后占用是否持续上升。百分比看起来较高,也需要结合实际容量和变化过程判断。

jstat 适合辅助观察,不宜把固定列位置写死成长期采集协议。官方将它标为实验性工具,并提示输出格式可能变化;已有监控应继续使用其受支持的采集方式。JDK 21 jstat 说明和 JDK 17 说明给出了字段及采样参数。

用 GC 日志找出具体暂停

优先保存已有 GC 日志和轮转文件。若原先没有日志,可在下一次经过审批的正常发布中,把下面的参数合并到现有 JVM 参数,放在 -jar 之前;它是一条 JVM 参数,不是单独执行的 Shell 命令:

-Xlog:gc*,safepoint:file=/var/log/java-api/gc-%p.log:time,uptime,level,tags:filecount=5,filesize=20M

目录要预先存在,并允许目标 Java 用户写入。%p 会展开为 JVM 的 PID;time、uptime 便于与请求时间对齐。轮转文件大小是近似目标,仍需保留磁盘余量、明确采集与保留范围。统一日志的选择器、修饰符和轮转规则见 Java 21 启动参数文档。

这条启动参数不会为已经发生的故障补出历史日志,也不应成为故障时盲目重启的理由。需要运行时调整时,先查看目标 JVM 自己支持的命令和现有输出:

jcmd "$APP_PID" help VM.log
jcmd "$APP_PID" VM.log list=true

上面两条只查看帮助和配置,不会启用新的日志输出。确需动态开启时,按该 JVM 的参数说明制定采集范围和结束条件,避免覆盖现有配置或长期启用大量 debug 日志。

分析时,先找到慢请求附近的 GC 事件、原因、顶层暂停耗时,以及回收前后的堆占用。同一个 GC ID 往往有多条阶段明细,不能把顶层暂停和内部阶段耗时一起累加;并发标记阶段的持续时间也不能直接当成应用全程停止的时间。

分清回收类型,再看占用为什么降不下来

G1 的 Young、Mixed 与 Full GC 所做的工作不同。先按日志里实际出现的类型和原因判断,不用“老年代高”“Full GC 出现过”这类单点现象直接判定内存泄漏。

日志与趋势下一步方向
Young 回收密集,但回收后占用能明显回落结合流量,查对象分配热点、批量大小与临时大结果集
Mixed 回收过程中,回收后占用仍持续抬高看存活对象、缓存边界、队列积压和并发周期是否跟得上分配
出现 Full GC 或疏散失败相关记录结合事件原因、堆余量、对象大小与前后周期继续定位
暂停的实际时间明显偏长同时查 CPU 调度、容器限流、宿主机内存和磁盘等环境证据

GC 日志中的 User、Sys、Real 含义不同:前两项反映 CPU 时间,Real 是经过的实际时间,不能混为一个耗时或直接相加当作请求停顿。多线程收集时 CPU 时间也不必等于实际时间。G1 调优指南专门讨论了这类延迟线索。

“回收后还留着很多对象”也可能来自业务允许的缓存或长生命周期数据。要证明不应存活的对象被保留,还需要对象及引用关系的证据。较大的堆转储、对象直方图或强制回收有各自影响,不能仅为让曲线好看就反复执行;先明确要回答的问题、采集窗口与资源预算。

用一段 JFR 连接暂停与分配热点

需要进一步对齐分配、线程活动和 GC 事件时,可在允许采样的窗口录制一小段 JFR。先检查是否已有录制,复用现成数据时就不重复启动:

jcmd "$APP_PID" JFR.check

TASK_STAMP=$(date +%Y%m%d-%H%M%S)
jcmd "$APP_PID" JFR.start \
  name="gc_diag_${APP_PID}_${TASK_STAMP}" \
  settings=profile duration=60s \
  filename="/var/log/java-api/gc-${APP_PID}-${TASK_STAMP}.jfr"

目录和剩余空间应事先准备好,路径按目标 JVM 能看到的文件系统解释。这里的 profile 比 default 收集更多信息,开销也更高,适合短时定位,不能假设对任何负载都没有影响。若已有持续录制,先了解其事件配置和覆盖时段,再决定是否需要补充。相关选项见 jcmd 的 JFR 命令说明。

命令返回成功只是录制启动的证据。等这次 60 秒录制结束、文件生成后,在同一会话中检查摘要;若拷贝到分析机,改为实际文件路径:

jfr summary "/var/log/java-api/gc-${APP_PID}-${TASK_STAMP}.jfr"

摘要可核对录制时段和事件概况,不直接给出业务根因。用 JDK Mission Control 或现有 JFR 分析工具,把 GC 暂停、分配热点和线程活动放到慢请求窗口里看:是否某个接口或后台任务制造大量短命对象,是否批量结果被长时间持有,还是时间主要花在等待。

采样结果受事件设置和观察窗口影响,没有相应事件不能直接证明没有该类活动。录制文件也可能包含业务路径和运行信息,按现有权限保存与传递。JDK jfr 工具文档说明了摘要和事件查看能力。

根据证据修改,再用同类负载复测

分配热点明确后,先评估批量处理、分页、重复转换与大响应构建;存活对象增长时,检查缓存上限、队列积压和引用生命周期。若是环境调度或容器预算导致暂停被拉长,要结合实例资源处理,不能只改堆大小。

G1 的 MaxGCPauseMillis 是软目标,并非每次暂停的硬上限。过早固定年轻代大小或搬来一组 GC 参数,可能限制收集器的自适应能力。堆容量也要放在进程和容器的总内存预算里考虑,不能把 -Xmx 当作进程全部内存。Java 参数说明和 G1 指南给出了这些边界。

每次修改都记录假设、版本和配置,再在可比的流量、数据量与时间窗口下,比较请求延迟分布、错误、暂停分布、分配与回收后占用。还要检查吞吐量、CPU 和总内存,防止某一项变好却把代价转移到其他地方。

如果暂停时间线仍对不上慢请求,回到 Java 后端排障专题,继续按线程、连接池、SQL 或外部调用定位。记录哪些证据已确认、哪些还缺少,比先选一组“万能参数”更便于下一步处理。

参考资料

更新记录

  • 2026-11-02:整理 GC 次数、具体暂停、G1 日志与短时 JFR 的定位顺序;按 JDK 官方资料核对命令及环境前提,未在生产 JVM 执行诊断、修改参数或验证性能改善。