项目中是如何选择垃圾回收器的?为啥选择这个?

#Java#面试八股

项目中是如何选择垃圾回收器的?为啥选择这个?

结论先行:选收集器只需三个输入——堆有多大、能容忍多长停顿、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 版本是硬约束。把选择理由写成「基线日志 + 停顿预算」的形式,面试和复盘都站得住。

笔记加载中…