项目中是如何选择垃圾回收器的?为啥选择这个?
结论先行:选收集器只需三个输入——堆有多大、能容忍多长停顿、JDK 版本是什么。默认答案:JDK 8 用 Parallel(吞吐优先)或 CMS(低停顿、堆 48G);JDK 11/17 用 G1(默认,堆 432G);堆超过 32G 且要求毫秒级停顿才上 ZGC。选择理由必须能落到「业务停顿预算 + 实测 GC 日志」,而不是「网上说 G1 好」。
一、主流收集器对照
| 收集器 | 分代 | 算法 | 停顿 | 适用堆 | 关键参数 |
|---|---|---|---|---|---|
| Serial / Serial Old | 是 | 复制 + 标记-整理 | 秒级 | < 100M,客户端 | -XX:+UseSerialGC |
| Parallel Scavenge / Old | 是 | 复制 + 标记-整理 | 数百毫秒 | 4~8G,批处理 | -XX:+UseParallelGC |
| CMS | 是 | 标记-清除(并发) | 数十毫秒,但有 Full GC 兜底 | 4~8G | -XX:+UseConcMarkSweepGC |
| G1 | 逻辑分代 | Region + 复制/整理 | 可设目标(默认 200ms) | 4~32G | -XX:+UseG1GC |
| ZGC | 分代(JDK 21+) | 染色指针 + 并发转移 | < 1ms(与堆大小基本无关) | 数十 G ~ TB | -XX:+UseZGC |
| Shenandoah | 分代(JDK 21+) | Brooks 指针 + 并发转移 | < 10ms | 大堆 | -XX:+UseShenandoahGC |
二、选择路径
堆 < 2G 且单核/客户端?
└─ 是 → Serial(简单、开销小)
└─ 否 → 停顿预算是多少?
├─ 不在意停顿,追求吞吐(离线批处理、报表) → Parallel
├─ 100ms 以内,堆 4~32G(绝大多数在线服务) → G1
├─ 10ms 以内,堆 > 32G(交易、撮合、风控) → ZGC / Shenandoah
└─ 停留在 JDK 8,堆 4~8G,且能接受碎片治理 → CMS
三、一次真实选型过程
假设订单服务:8C16G 容器、堆 6G、P99 延迟要求 200ms、JDK 17。
# 第一轮:默认 G1,记录基线
java -Xms6g -Xmx6g -XX:MaxRAMPercentage=75 \
-XX:+UseG1GC -XX:MaxGCPauseMillis=150 \
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime:filesize=50m,filecount=5 \
-jar order.jar
# 第二轮:若年轻代回收过于频繁,调整 Region 与新生代比例
java -Xms6g -Xmx6g -XX:+UseG1GC \
-XX:G1NewSizePercent=20 -XX:G1MaxNewSizePercent=40 \
-XX:G1HeapRegionSize=8m -XX:MaxGCPauseMillis=150 -jar order.jar
# 第三轮(非常规):若延迟要求升到 10ms 且堆扩到 32G
java -Xms32g -Xmx32g -XX:+UseZGC -XX:+ZGenerational -jar order.jar
对比依据只看三件事:Young GC 频率与耗时、Mixed GC 是否追得上分配速率、是否出现 Full GC。日志里 Pause Young 平均 30ms、Pause Full 为 0,就说明 G1 选对了。
四、按场景给结论
| 场景 | 推荐 | 理由 |
|---|---|---|
| 内部管理后台、定时任务 | Parallel | 吞吐高、GC 线程占满 CPU 也不影响用户体验 |
| 对外 API、堆 8G 左右 | G1 | 可设停顿目标,避免 Full GC 长停顿 |
| 秒杀、竞价、风控 | ZGC | 停顿与堆大小解耦,尾延迟稳定 |
| 大数据离线计算 | Parallel | 吞吐优先,停顿无所谓 |
| 遗留 JDK 8 系统 | CMS 或 G1(8u 后期可用) | CMS 有碎片与并发模式失败风险,优先 G1 |
五、常见追问
| 追问 | 回答要点 |
|---|---|
| JDK 8 默认是什么 | Parallel Scavenge + Parallel Old;JDK 9 起默认 G1 |
| 为什么大堆偏爱 ZGC | 染色指针 + 读屏障让标记与转移都能并发,停顿不再随堆线性增长 |
| G1 一定比 CMS 好吗 | 小堆(< 4G)下 CMS 停顿可能更短;G1 的优势是无碎片、可预测停顿 |
| 容器里怎么设堆 | 用 -XX:MaxRAMPercentage=75 而不是写死 -Xmx,JDK 8u191+ 才认 cgroup 限制 |
| 换了收集器还要调什么 | 停顿目标、Region 大小、并发线程数(-XX:ParallelGCThreads、-XX:ConcGCThreads) |
六、用压测验证选型
# 固定压力,跑 30 分钟,只对比 GC 指标
wrk -t8 -c200 -d1800s --latency http://127.0.0.1:8080/api/order
# 跑完统计:停顿总时长 / 次数 / 最大值
grep -E "Pause (Young|Full)" /var/log/app/gc.log \
| awk '{for(i=1;i<=NF;i++) if($i=="ms") print $(i-1)}' \
| awk '{n++; s+=$1; if($1>m) m=$1} END {printf "n=%d total=%.0fms avg=%.1fms max=%.0fms\n", n, s, s/n, m}'
对比口径必须一致:同一份压测脚本、同一个堆上限、同一份流量回放。只看「平均停顿」会掩盖长尾,务必同时看 P99 与最大值,因为业务超时往往就发生在那一两次长停顿上。
小结
选型不是选「最强」,而是选「最匹配」:吞吐敏感用 Parallel,延迟敏感用 G1/ZGC,堆大小与 JDK 版本是硬约束。把选择理由写成「基线日志 + 停顿预算」的形式,面试和复盘都站得住。