最后更新: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 或外部调用定位。记录哪些证据已确认、哪些还缺少,比先选一组“万能参数”更便于下一步处理。
参考资料
- JDK 21:G1 调优
- JDK 21:jcmd
- JDK 17:jcmd
- JDK 21:jstat
- JDK 17:jstat
- JDK 21:Java 参数与统一日志
- JDK 21:jfr 文件分析工具
更新记录
- 2026-11-02:整理 GC 次数、具体暂停、G1 日志与短时 JFR 的定位顺序;按 JDK 官方资料核对命令及环境前提,未在生产 JVM 执行诊断、修改参数或验证性能改善。
Java GC 频繁、接口停顿怎么查
https://java.li/archives/java-gc-pauses-troubleshooting
评论