JVM 中一次完整的 GC 流程是怎样的?
结论先行:以 G1 为例,一次完整的 GC 周期不是「一次停顿」,而是Young GC → 并发标记周期 → 混合回收(Mixed GC)→ 兜底 Full GC 的组合。对象先在 Eden 分配,Eden 满触发 Young GC;当老年代占用超过阈值(默认 45%)启动并发标记,标记完成后进入若干轮混合回收,逐步清掉老年代中垃圾占比高的 Region;如果回收速度追不上分配速度,才会退化成一次 Full GC。
一、完整周期
对象分配(Eden / TLAB)
│ Eden 满
▼
① Young GC(STW):Eden + Survivor 存活对象 → 新 Survivor 或晋升
│ 老年代占用 ≥ InitiatingHeapOccupancyPercent(默认 45%)
▼
② 并发标记周期
├─ 初始标记(STW,借一次 Young GC 完成)
├─ 根区域扫描(并发)
├─ 并发标记(并发,SATB 写前屏障)
├─ 重新标记(STW,处理 SATB 队列)
└─ 清理(并发,统计各 Region 存活率)
▼
③ 混合回收(多轮 Mixed GC):Young Region + 垃圾占比高的 Old Region
│ 若晋升速率 > 回收速率,或 Humongous 分配失败
▼
④ Full GC(STW,单线程标记-整理,兜底)
二、各阶段在做什么
| 阶段 | 是否 STW | 主要动作 | 日志关键字 |
|---|---|---|---|
| Young GC | 是 | 复制 Eden/Survivor 存活对象,处理晋升 | Pause Young (Normal) |
| 初始标记 | 是 | 标记 GC Roots 直接可达对象 | Pause Young (Concurrent Start) |
| 并发标记 | 否 | 遍历对象图,SATB 记录引用删除 | Concurrent Mark Cycle |
| 重新标记 | 是 | 处理 SATB 缓冲,完成最终标记 | Pause Remark |
| 清理 | 否 | 统计 Region 存活率,回收全空 Region | Concurrent Cleanup |
| 混合回收 | 是 | 回收 Young + 部分 Old Region | Pause Young (Mixed) |
| Full GC | 是 | 整堆标记-整理,压缩 | Pause Full |
三、日志里的一个真实周期
[2.315s][info][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 512M->48M(1024M) 12.431ms
[5.902s][info][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 560M->96M(1024M) 15.207ms
[8.441s][info][gc] GC(2) Pause Young (Concurrent Start) (G1 Evacuation Pause) 604M->148M(1024M) 18.002ms
[8.441s][info][gc] GC(2) Concurrent Mark Cycle
[9.120s][info][gc] GC(2) Pause Remark 172M->172M(1024M) 2.114ms
[9.402s][info][gc] GC(2) Pause Cleanup 176M->176M(1024M) 0.089ms
[9.883s][info][gc] GC(3) Pause Young (Mixed) (G1 Evacuation Pause) 420M->132M(1024M) 21.553ms
三段信息要会读:512M->48M(1024M) 是「回收前 → 回收后(堆总容量)」,Pause Young (Concurrent Start) 表示这次 Young GC 顺带启动了并发标记,Mixed 说明这一轮带上了老年代 Region。
四、让一次完整周期跑出来
# 小堆 + 低阈值,几秒内就能看到完整的 Young / Mixed / Full 链路
java -Xms256m -Xmx256m \
-XX:+UseG1GC \
-XX:InitiatingHeapOccupancyPercent=20 \
-XX:G1HeapRegionSize=4m \
-Xlog:gc*:stdout:time,uptime \
-jar gc-demo.jar
# 想观察兜底 Full GC:让晋升速率超过回收速率即可
java -Xms128m -Xmx128m -XX:+UseG1GC -XX:G1ReservePercent=5 -jar gc-demo.jar
五、常见追问
| 追问 | 回答要点 |
|---|---|
| 为什么初始标记要借一次 Young GC | G1 利用 Young GC 的 STW 窗口顺带完成,避免额外停顿,所以日志里显示 (Concurrent Start) |
| SATB 解决什么问题 | 并发标记期间引用被删除时,按快照把旧引用记下来,保证不漏标 |
| 混合回收会回收所有 Old Region 吗 | 不会,只回收「垃圾占比高于阈值」的 Region,分多轮完成,这就是 MaxMixedGcCount 的意义 |
| Full GC 一定是灾难吗 | 是信号不是原因:说明收集器已经失控,要查 Humongous 分配、晋升速率、-XX:G1ReservePercent |
| CMS 的完整流程有何不同 | CMS 没有 Mixed 概念,直接「初始标记 → 并发标记 → 重新标记 → 并发清除」,兜底是带压缩的 Full GC |
小结
一次完整 GC 周期是「复制回收新生代 → 并发标记老年代 → 分批混合回收 → 失败则整堆兜底」。能把日志行对应到阶段,就已经超过大多数只会背名词的答案了。