2602 字
约 8 分钟
17
S05-X27 · GC 原因码与 CMS/G1 的实现细节
2024-12-31
2026-11-09

S05-X27 · GC 原因码与 CMS/G1 的实现细节

调 GC 参数之前,先读懂日志那行括号。-Xlog:gc 里的 (Allocation Failure)、(Metadata GC Threshold)、(GCLocker Initiated GC) 对应完全不同的触发路径:前者要动堆与晋升策略,后者得查 JNI 与直接内存。把原因码当成"GC 又发生了",参数只会越调越乱。

这篇按四层走:原因码从哪来 → CMS 与 G1 各自怎么算回收目标 → 发行版与线程视角的差异 → 什么时候该换工具。


1. 原因码长什么样:先建立日志读法

$ java -Xms4g -Xmx4g \
    -Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=10,filesize=50m \
    -jar app.jar

日志行按 JDK 17 的 -Xlog 格式对齐后大致如下:

[2026-11-09T09:12:03.117+0000][12.345s][info][gc] GC(37) Pause Young (Normal) (G1 Evacuation Pause) 512M->128M(4096M) 8.412ms
[2026-11-09T09:12:09.884+0000][19.112s][info][gc] GC(38) Pause Full (Metadata GC Threshold) 1024M->640M(4096M) 412.556ms
字段 含义 判据价值
GC(37) 第几次回收 与 uptime 相除即回收频率
Pause Young (Normal) 分代 + 回收类型 判断是不是混合回收
(G1 Evacuation Pause) G1 内部动作名 区分 Normal 与 Concurrent Start
512M->128M(4096M) 回收前→回收后(总容量) 判断这次回收有没有收益
8.412ms STW 时长 直接对齐 SLA

Pause Full 与 Pause Young 的停顿差一到两个数量级(典型量级)。看到 Pause Full 混在 Young 之间,先抓 cause 再谈参数。


2. GCCause:十几个原因码,各有各的处理方向

原因码 触发条件 是否正常 处理方向
Allocation Failure 目标分区难以完成分配 正常,看频率 加大 Young 区或降低分配速率
Metadata GC Threshold Metaspace 水位触线 不正常 查动态类生成、类加载器泄漏
Ergonomics 自适应策略起的首次回收 正常 只作为基线
GCLocker Initiated GC JNI 临界区退出后补一次回收 需关注 查 JNI 与直接内存占用
System.gc() 显式调用(含三方库间接调用) 不正常 查代码与依赖,必要时 -XX:+DisableExplicitGC
Heap Dump Initiated GC jcmd GC.heap_dump 前做清理 人工触发 从统计里排除
Last ditch collection 清软引用前的最后一次尝试 已到 OOM 边缘 立刻留堆快照
WhiteBox Initiated GC 测试框架触发 仅测试环境 生产出现即异常

Last ditch collection 是最后一道闸:抛 OutOfMemoryError 前 HotSpot 会清掉所有软引用,见到它就该带 -XX:+HeapDumpOnOutOfMemoryError 拿现场。


3. CMSHeap:并发回收的代际结构

CMS 的代际结构是 ParNew(新生代)+ CMS(老年代,并发标记清除),堆在逻辑上仍是 Eden / Survivor / Old 三段,回收器线程与用户线程并发跑标记与清除。

$ java -Xms4g -Xmx4g -Xmn1g \
    -XX:+UseConcMarkSweepGC -XX:+UseParNewGC \
    -XX:CMSInitiatingOccupancyFraction=70 -XX:+UseCMSInitiatingOccupancyOnly \
    -XX:+CMSParallelRemarkEnabled -XX:+CMSClassUnloadingEnabled \
    -Xlog:gc* -jar app.jar
参数 作用 不配会怎样
-XX:+UseConcMarkSweepGC 启用 CMS JDK 14 起已移除,加不上
CMSInitiatingOccupancyFraction 老年代占用到多少开始并发标记 默认阈值易触发并发模式失败
+UseCMSInitiatingOccupancyOnly 固定用上面的比例 阈值被反复改,行为不可预测
CMSParallelRemarkEnabled 并行重标记 重标记阶段停顿拉长
CMSClassUnloadingEnabled 并发标记时顺带卸类 Metaspace 只涨不落

CMS 最典型的坑是并发模式失败:并发回收没跑完老年代已满,退化成一次 Full GC,日志表现为 (concurrent mode failure) 与长停顿同时出现。方向两个:提前 CMSInitiatingOccupancyFraction,或压晋升速率(加大 Young 区、降低存活率)。

CMSHeap 的形态随版本变化:JDK 9 起废弃并打警告,JDK 14 移除。参数是否还存在,按对应版本文档核对。


4. G1MixedGCCountTarget:G1 怎么决定"摸几个老年代分区"

G1 把堆切成等大 Region;Young GC 后堆占用越过 IHOP(Initiating Heap Occupancy Percent)就启动并发标记,标记结束进入混合回收——回收全部新生代分区 + 一部分老年代分区,后者分批做完,批数由 G1MixedGCCountTarget 控制。

$ java -Xms8g -Xmx8g -XX:+UseG1GC \
    -XX:InitiatingHeapOccupancyPercent=45 \
    -XX:G1MixedGCCountTarget=8 \
    -XX:G1HeapWastePercent=5 \
    -XX:G1MixedGCLiveThresholdPercent=85 \
    -XX:MaxGCPauseMillis=200 \
    -Xlog:gc* -jar app.jar
参数 默认值方向 调大的后果 调小的后果
G1MixedGCCountTarget 8 每批收回的分区更少,单次停顿更短,但混合回收阶段拖得更长 单次混合回收更重,停顿更陡
G1HeapWastePercent 5 允许更多垃圾留在堆里不回收,混合阶段更早结束 回收更激进,混合阶段更长
G1MixedGCLiveThresholdPercent 85 存活率高于阈值的分区不纳入本次回收 更多分区被回收,停顿更重
InitiatingHeapOccupancyPercent 45 标记启动更早,并发失败风险更低,CPU 占用更高 启动更晚,容易退化成 Full GC
MaxGCPauseMillis 200 G1 以此为软目标反推 Region 数量与批数 目标过小会导致回收追不上分配

G1MixedGCCountTarget 是上限式目标,不是每批必须回收的分区数:G1 在 MaxGCPauseMillis 约束下按"最划算优先"挑分区,总批数尽量不超过该值。

验证是否进入混合回收:看日志里的 Pause Young (Mixed),或 jcmd 5123 PerfCounter.print | grep -i mixed。


5. GCLocker:JNI 临界区如何把 GC 卡住

GCLocker Initiated GC 背后是 GCLocker 机制:线程经 JNI 取原始指针(GetPrimitiveArrayCritical / GetStringCritical)时进入临界区,区内 GC 不能移动对象,HotSpot 挂起请求并记一笔"待办",等最后一个临界区退出时补回收。

现象 机制 处理
日志里零星出现 GCLocker Initiated GC 临界区退出后补回收 正常,观察频率
连续多条且伴随长停顿 多个线程长驻临界区 找 JNI 调用点,缩短临界区
出现 Failed to allocate + 该原因码 临界区内分配失败 查本地库与直接内存
堆外占用高但 GC 不动 GCLocker 与直接内存释放互相等待 结合 BufferPool 与堆外统计

直接内存的释放依赖 Cleaner(DirectByteBuffer 的清理器),其引用处理需要能安全移动对象;临界区长期不退出就会拉长这条链。看到 GCLocker 顺手看直接内存:

$ jcmd 5123 VM.native_memory summary   # 需启动时加 -XX:NativeMemoryTracking=summary
$ java -XX:MaxDirectMemorySize=512m -XX:+UseG1GC -jar app.jar

排查线程是否卡在临界区,用带锁信息的线程栈:

$ jcmd 5123 Thread.print -l > /tmp/th-$(date +%H%M).txt

栈里出现 GCLocker::stall_until_clear、jni_GetPrimitiveArrayCritical 之类的帧即这条路径,多来自本地库或加密原生实现。


6. 发行版与线程视角:Temurin-17.0.12、OSThread 与 NIO.2

同一 -XX 参数在不同发行版与大版本上可用性不一致,以 Temurin 为例:

$ java -version
openjdk version "17.0.12" 2026-07-16
OpenJDK Runtime Environment Temurin-17.0.12+7 (build 17.0.12+7)
OpenJDK 64-Bit Server VM Temurin-17.0.12+7 (build 17.0.12+7, mixed mode, sharing)
视角 观察方式 说明
发行版 java -version 第三行是否带 Temurin 发行版绑定补丁级别,Temurin-17.0.12 与 17.0.9 的 GC 修复不同
可用参数 -XX:+PrintFlagsFinal 里搜参数名 JDK 17 已无 CMS 参数,G1 齐全
线程单元 jstack 里的 GC Thread#N 回收线程属 JVM 内部,HotSpot 以 OSThread/JavaThread 两类形态管理
语言级线程 栈里的虚拟线程(JDK 21+) JDK 17 无此特性,别按 21 的文档核对
文件 IO NIO.2 的 Files/Path/异步通道 异步文件通道由线程池承载,池满会表现为"GC 正常但吞吐不动"

GC Thread# 不属于用户线程池,别在"线程数上限"里给它留位;真正抢 CPU 的是 -XX:ParallelGCThreads 与 -XX:ConcGCThreads。

NIO.2 在 GC 视角下意味着分配位置:Files.readAllBytes、ByteBuffer.allocateDirect 的字节不进 Java 堆,堆很健康而 RSS 在涨:

$ jcmd 5123 System.gc   # 仅在取证时使用,触发软引用/清洁器处理,判断堆外是否可回收

7. 排错:六种日志读不通的情况

现象 原因 处理
查不到 cause -Xlog 只开 gc 没开 gc* 加 -Xlog:gc*,确认 tags 已输出
只有 Allocation Failure 却频繁 Full GC 晋升失败记在别处 开 -Xlog:gc+ergo+phases 看晋升
混合回收从不出现 没过 IHOP,或 G1HeapWastePercent 过宽 先降 IHOP 再收紧阈值
加了 CMS 参数启动失败 该 JDK 已移除 CMS PrintFlagsFinal 核对,改用 G1
停顿与日志对不上 时间戳是墙钟,STW 期间也计入 用 uptime 差值交叉验证
堆正常但 RSS 持续涨 堆外:直接内存、Metaspace、线程栈 NMT + GC.heap_info + 容器指标三处对齐

8. 边界:什么情况这套读法不适用

场景 判断 该用什么
低延迟硬约束(P99 毫秒级) MaxGCPauseMillis 只是软目标 ZGC / Shenandoah,或改架构削峰
堆小于 2G 且对象小 G1 的分区管理与记忆集带来额外开销 老老实实用 G1 或回退到分代串行
容器内堆大于配额 要按容器配额而非宿主内存算 用 MaxRAMPercentage 核对
停顿来自 safepoint GC 停顿短但应用卡顿 开 -Xlog:safepoint 定位去优化
需要毫米级归因 日志粒度不够 JFR + async-profiler

一句话:原因码定方向,回收器定手法,堆外定你看到的数字是否完整。


10. 自测

  1. -Xlog:gc 里只有 Allocation Failure,却频繁出现 Full GC,该追加哪个标签去看?
  2. 看到 Last ditch collection 说明进程已经走到哪一步?
  3. 只配了 CMSInitiatingOccupancyFraction 而没配 UseCMSInitiatingOccupancyOnly,行为为什么不可预测?
  4. G1MixedGCCountTarget 调到 20 为什么不会让回收更快?
  5. GCLocker Initiated GC 反复出现,应该往哪两个方向查?
  6. 堆日志健康但进程 RSS 持续上涨,要取哪三处数据对齐?
S05-X27 · GC 原因码与 CMS/G1 的实现细节
https://www.clxhxhhr.top/posts/6017/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。