JVM GC垃圾回收详解:7种收集器选型+CMS/G1/ZGC对比+生产调优参数模板 JVM GC 与调优7 种垃圾收集器选型 参数调优实战问题场景监控告警——Full GC 每 10 分钟触发一次STW 超过 2 秒。改了几个 JVM 参数频次降了但吞吐量也降了。GC 调优不是调大 Xmx——是理解每种收集器的设计目标和适用边界。30秒速览GC 调优第一件事不是调参数——是选对收集器。选型口诀堆 4G→Parallel4-32G→G132G 且 1ms STW→ZGC。选错了收集器参数怎么调都是错的。7 种 GC 速查表 生产参数模板可直接抄到生产环境。本文是《Java 后端核心知识图谱》系列第 5 篇共 172 篇。一、判断对象存活两种算法算法一引用计数法每个对象维护一个计数器被引用一次 1引用失效 -1归零即回收。Person p1 new Person(); // 对象#001: 引用计数 1 Person p2 p1; // 对象#001: 引用计数 2 p1 null; // 对象#001: 引用计数 1 p2 null; // 对象#001: 引用计数 0 → 回收 ✓优点实时回收无 STW——引用归零立即释放缺点循环引用无法解决每次赋值都要更新计数高并发下性能差循环引用——引用计数的致命缺陷classNode{Nodenext;}NodeanewNode();NodebnewNode();a.nextb;// b 计数 2b.nexta;// a 计数 2anull;// a 计数 1仍被 b.next 引用bnull;// b 计数 1仍被 a.next 引用// a 和 b 互相指着计数器永远1从 GC Roots 出发谁也到不了 → 垃圾永存谁在用引用计数Python引用计数 标记清除检测循环、Cshared_ptr/weak_ptrweak_ptr 不增加计数打破循环、SwiftARCweak 引用打破循环。为什么这些语言用实时释放、无 STW——C 没 GCiOS 内存小不能容忍 GC 暂停。算法二可达性分析Java 使用从GC Roots出发沿引用链搜索能到达的 活的到不了的 可回收。GC Roots 有哪些口诀“栈上的、静态的、常量的、JNI 的、锁着的”虚拟机栈栈帧中的局部变量表引用的对象方法区中静态变量引用的对象运行时常量池引用的对象JNI 引用的对象被 synchronized 持有的对象从每个 Root 出发沿着引用链一路找下去 GC Root ──→ 对象A ──→ 对象B ──→ 对象C ← 活可以从 Root 到达 对象D ← 死无人能到达 对象E ← 死核心优势天然免疫循环引用——从 GC Roots 出发谁也到不了 a 和 b双双判定为死。维度引用计数法可达性分析原理每个对象计数归零回收GC Roots 出发不可达 可回收实时性即时释放无 STW需等 GC 触发有 STW循环引用❌ 需额外机制✓ 天然免疫性能开销每次赋值更新计数只在 GC 时扫描平时零开销代表语言Python, C, SwiftJava, Go, C#二、四种引用类型引用类型回收时机适用场景强引用Strong永不回收正常Object o new Object()软引用Soft内存不够时回收缓存——OOM 前自动释放弱引用WeakGC 时无论内存是否够都回收WeakHashMap、ThreadLocal 的 key防内存泄漏虚引用Phantomget() 永远返回 null对象回收时收到通知配合 ReferenceQueue 做堆外内存释放NIO Cleaner三、分代收集理论两个统计假设弱分代假设弱分代假说IBM 研究——98% 的对象朝生夕死活不过第一次 GC强分代假说熬过越多次 GC 的对象越不容易死亡分代的好处新生代用复制算法存活少复制成本低老年代用标记-清除/整理算法存活多移动成本高各用最适合的算法。堆分代结构堆内存 新生代 老年代 新生代 Eden Survivor0 Survivor1 默认比例新生代:老年代 1:2 默认比例Eden:S0:S1 8:1:1 为什么 8:1:198% 对象朝生夕死Minor GC 后约 2% 存活10% Survivor 足够容纳 为什么两个 Survivor复制算法要求一块已用 一块空交替使用极端情况由老年代兜底分配担保对象流转过程新对象 → Eden 分配优先用 TLAB 无锁分配 ↓ Eden 满Minor GC 存活对象 → 复制到空闲 Survivor年龄1Eden 旧 Survivor 清空 ↓ 每次 Minor GC两个 Survivor 交替年龄1 年龄 ≥ 15 → 晋升老年代 ↓ 老年代满 触发 Full GC晋升老年代的五种途径途径条件年龄达标Minor GC 每次年龄1到达MaxTenuringThreshold15对象头 age 字段 4 bit最大 15动态年龄判断Survivor 中同龄对象总大小 Survivor 一半 → 该年龄及以上全部晋升Survivor 放不下Minor GC 后目标 Survivor 不够用 → 多余对象直接进老年代分配担保大对象直接分配超过-XX:PretenureSizeThreshold→ 直接在老年代分配避免在 Eden/Survivor 间来回复制空间分配担保Minor GC 前检查老年代连续空间。不够 → Full GC四、三色标记法 漏标问题CMS 和 G1 的并发标记阶段使用三色标记三种颜色含义 白色White→ 还没被 GC 碰过未知死活起始全白 灰色Gray → GC 已碰到但下游对象还没检查完 黑色Black→ 该对象 所有下游都检查完了 流转过程 初始所有对象 ○白色 ① 标记 GC Roots 直接引用的对象为灰色 ●A ○B ○C 灰色队列 [A] │ ○D ② 取出 A检查 A 的引用 → D 变灰A 变黑 ◎A ○B ○C 灰色队列 [D] │ ●D ③ 取出 DD 无引用 → D 变黑 ◎A ○B ○C 灰色队列 空 → 标记完成 │ ◎D 结果黑色(A、D) 活白色(B、C) 死可回收规则不能直接从白变黑必经灰色灰色队列清空 标记结束。并发标记漏标问题两个条件同时满足→ 漏标活对象被错误回收黑色对象新增了对白色对象的引用灰色对象删除了对该白色对象的引用收集器解决机制原理CMS增量更新Incremental Update黑色对象新增引用时重新记灰 → 并发标记后重新扫描这些对象G1SATBSnapshot At The BeginningGC 开始时做逻辑快照假设都活着删除引用时记录到 SATB 队列 → 事后处理五、7 种垃圾收集器选型分代收集器1Serial / Serial Old新生代 Serial单线程、复制算法。老年代 Serial Old单线程、标记-整理适用100MB 堆、桌面应用、嵌入式。小堆场景反而是最快的单线程无线程切换开销参数-XX:UseSerialGC2ParNew新生代多线程 Serial老年代配 CMS。JDK 9 废弃、JDK 14 移除被 G1 替代3Parallel Scavenge / Parallel OldJDK 8 默认新生代 老年代都是多线程。核心目标吞吐量优先STW 时间/总运行时间最小化适用后台批处理、科学计算等不关心单次停顿只关心总吞吐的场景参数-XX:UseParallelGC、-XX:MaxGCPauseMillis停顿目标vs-XX:GCTimeRatio吞吐量目标两者互斥4CMSConcurrent Mark SweepJDK 14 移除老年代收集器标记-清除算法。核心目标最短 STWGC 线程和用户线程并发执行四阶段① 初始标记STW极短→ ② 并发标记无 STW长→ ③ 重新标记STW较短→ ④ 并发清除无 STW长致命缺陷标记-清除产生内存碎片→ 碎片多到无法分配 → 退化为 Serial Old 单线程 Full GC极慢浮动垃圾并发标记期间新产生的垃圾等下一轮对 CPU 敏感参数-XX:UseConcMarkSweepGC、-XX:CMSInitiatingOccupancyFraction70不分代收集器5G1Garbage FirstJDK 9 默认⭐革命性设计——Region 化内存布局传统堆 两间固定房间新生代 老年代 G1 堆 ~2048 个可移动隔断的格子每个按需当 Eden/Survivor/Old/Humongous 类比停车场传统是东半边临时车位、西半边长期车位——打扫西半边必须全扫。 G1 是每个车位可随时改类型——哪个脏了清理哪个扫完清空下次当临时车位用。G1 如何筛选 Region——Garbage First名字的由来① 并发标记阶段统计每个 Region 的存活字节数live_bytes ② 计算性价比 垃圾量 / 存活对象量 ③ 从性价比最高的 Region 开始回收直到预测时间接近 MaxGCPauseMillis ④ 性价比低的 Region 留等下次多攒点垃圾再收更划算三种 GC 模式模式触发回收范围Young GCEden Region 满仅 EdenSTWMixed GCG1 特有堆占用达 IHOP默认 45%所有 Eden 按性价比筛选的部分 Old RegionFull GC最坏情况Mixed GC 来不及 / 碎片化 / Metaspace 满全堆退化为 Serial Old 单线程 → STW 极长关键概念RSetRemembered Set每个 Region 维护谁引用了我。通过写屏障Write Barrier维护——每次obj.field newValue时 JIT 插入代码更新 RSet。Minor GC 时只需查 RSet不用全堆扫描CSetCollection Set本轮要回收的 Region 集合选性价比最高的SATB并发标记阶段解决漏标GC 开始时对堆做逻辑快照参数-XX:UseG1GC、-XX:MaxGCPauseMillis200期望最大停顿不硬保证、-XX:InitiatingHeapOccupancyPercent456ShenandoahJDK 15 生产基于 Region与 G1 的核心区别并发回收——复制对象的同时用户线程继续运行通过 Brooks Pointer 转发指针实现核心目标停顿与堆大小无关不管 10GB 还是 100GB停顿相近适用16GB 大堆 低延迟7ZGCJDK 15 生产染色指针Colored Pointers技术——在 64 位指针上直接编码 GC 状态不需额外元数据核心目标亚毫秒级停顿1ms无论堆大小TB 级堆也保持低延迟适用TB 级堆 超低延迟游戏服务器、金融交易系统参数-XX:UseZGC选型速查GC目标堆大小STW适用场景Serial单线程简单100MB长桌面应用、嵌入式Parallel吞吐量4GB较长批处理、后台计算CMS低延迟8GB较短已淘汰遗留系统G1平衡延迟/吞吐4-64GB可控Web 服务首选JDK 9Shenandoah超低延迟16GB极短大堆低延迟ZGC亚毫秒延迟TB 级1ms超大堆超低延迟不同 GC 的核心区别在并发程度——哪些阶段 STW哪些阶段并发。CMS初始重新标记 STW→ G1Young GC 仍 STW但按 Region 分批回收老年代→ ZGC全阶段并发只极短 STW 做根扫描。六、G1 生产调优实战关键参数速查参数默认值含义何时调-XX:MaxGCPauseMillis200期望最大 STWms非硬保证接口 RT 敏感时调低100ms-XX:G1HeapWastePercent5允许浪费的堆空间 %内存紧张时调低3%-XX:G1MixedGCLiveThresholdPercent85Region 存活对象超此比例→不回收老年代晋升快时调高90%更积极回收-XX:G1NewSizePercent5Young Gen 占堆最小%年轻对象多时调大10%减少过早晋升-XX:InitiatingHeapOccupancyPercent45老年代占比超此→触发并发标记调低35%让 Mixed GC 更早介入案例一Full GC 每 3 分钟一次接口 RT 从 50ms 飙升到 2s症状支付接口 RT 从平时 50ms 飙升到 2sjstat 发现 Full GC 每 3 分钟一次每次 STW 约 1.5s。排查jstat-gcpid1000# 每秒看 GC# 发现Eden 1 秒从空到满 → 高频年轻对象# Survivor 100% → 对象直接晋升老年代# 老年代增长 80MB/分钟 → 3 分钟触发 Full GCjmap-histo:livepid# 找到元凶# 支付回调 Map 缓存未设上限每次回调塞一个对象从不清理临时 GC 参数调整代码修复之前缓冲-XX:MaxGCPauseMillis150# 降低 STW 目标-XX:G1NewSizePercent10# 增大 Young Gen → Survivor 更大 → 减少过早晋升-XX:InitiatingHeapOccupancyPercent35# 更早开始并发标记 → 更早 Mixed GC效果Full GC 频率从每 3 分钟降到约 10 分钟一次。根本修复代码加 Map 容量上限 LRU 淘汰LinkedHashMap(10000, 0.75f, true)重写removeEldestEntry。案例二CMS Concurrent Mode Failure → 数秒长停顿症状线上服务每隔一段时间出现几秒长停顿GC 日志出现Concurrent Mode Failure。根因CMS 并发回收时对象分配晋升速度 CMS 回收速度 → 老年代满了 CMS 还没扫完 → 退化为 Serial Old 单线程整理几 GB 老年代 → STW 从几十 ms 跳到好几秒。解决# ① 调低触发阈值给 CMS 留更多时间-XX:CMSInitiatingOccupancyFraction60# 从 70% 降到 60%# ② 调大 Survivor减少过早晋升-XX:SurvivorRatio6# 从 8 调到 6# ③ 终极方案换 G1——按 Region 分批回收不会整块来不及扫-XX:UseG1GCGC 调优黄金原则先优化代码再调 GC——GC 参数是补丁不是根治。优化顺序内存泄漏 → 对象生命周期 → GC 参数一次只改一个参数——改多个分不清谁起效压测验证——生产级流量压测至少 30 分钟jstat 监控 GC 频率和 STWG1 的 MaxGCPauseMillis 不是硬保证——对象分配速度超过回收速度G1 还是退化为 Full GC记录 GC 日志-Xlog:gc*:filegc.log:time,uptime:filecount5,filesize10M事后用 GCeasy 分析七、知识串联一口气讲完内存与 GC介绍 JVM 的内存模型不是一个背概念题而是考察能不能把内存 GC 收集器三个知识点串联起来。标准叙述分四段第一段 — 6 个运行时数据区线程私有的三个程序计数器、虚拟机栈、本地方法栈 线程共享的三个堆、方法区/Metaspace、运行时常量池 直接内存。JDK 8 关键变化PermGen→Metaspace堆外消除了固定大小的 PermGen OOM。第二段 — 堆分代 对象流转98% 对象朝生夕死 → 分代设计。新生代 Eden:S0:S18:1:1复制算法。对象年龄满 15 或动态年龄判断晋升老年代。五种晋升途径要能说全。第三段 — 可达性分析 三色标记Java 用可达性分析对比引用计数法——Python/Swift 用循环引用是致命缺陷。三色标记是并发标记的核心工具。漏标两个条件同时满足CMS 用增量更新、G1 用 SATB 分别解决。第四段 — 收集器选型Serial小堆→ Parallel吞吐量JDK 8 默认→ CMS低延迟已淘汰→ G1Region 化 可预测停顿JDK 9 默认Web 服务首选→ ZGC染色指针亚毫秒停顿TB 级堆。核心要点回顾可达性分析 vs 引用计数——JVM 选择可达性分析的根源在于引用计数法无法处理循环引用A↔B 互相引用但无外部可达时计数永不为零造成内存泄漏而可达性分析从 GC Roots 出发沿引用链遍历不可达即垃圾天然免疫此问题。三色标记法以白未标记、灰已标记但子引用未扫描、黑已标记且子引用已扫描三种状态驱动并发标记的推进。漏标发生的充要条件是黑色对象新增了指向白色对象的引用同时灰色对象删除了指向该白色对象的引用——CMS 用增量更新写屏障将新增引用记录到 mod-union tableremark 阶段重新扫描解决G1 用 SATB写屏障在引用断裂前将旧值记录到 SATB 队列remark 阶段重新扫描曾经被引用过的对象解决。对象晋升有五条路径年龄达到 15默认 MaxTenuringThreshold、动态年龄判定同龄对象占用超过 Survivor 一半时该年龄及以上全部晋升、Survivor 空间不足Minor GC 时存活对象超出 Survivor 容量直接进老年代、大对象超过 G1HeapRegionSize/2 直接分配到 Humongous Region、空间分配担保老年代剩余空间不足以容纳可能晋升的全部对象时触发 Full GC。G1 的核心设计是将堆划分为约 2048 个大小相等的 Region每个 Region 可在逻辑上切换为 Eden/Survivor/Old/Humongous 四种角色通过 RSet记忆集——记录谁引用了我避免 GC 时全堆扫描和 CSet回收集合——Mixed GC 中除全部 Young Region 外还包含垃圾占比最高的部分 Old Region实现 Garbage First——优先回收垃圾最多的 Region性价比最大化。GC 演进沿着吞吐→延迟→超大堆的维度推进Serial客户端小堆→ Parallel吞吐量优先JDK 8 默认→ CMS并发低延迟但碎片化已标记废弃→ G1Region 化 可预测停顿JDK 9 默认Web 服务首选→ ZGC染色指针 亚毫秒停顿TB 级堆。Web 服务默认使用 G1-XX:UseG1GC调优的核心参数是MaxGCPauseMillis期望的最大停顿时间和InitiatingHeapOccupancyPercent触发并发标记周期的堆占用阈值。调优黄金原则先优化代码减少对象分配速率 分配合适的堆大小 选择合适的 GC 微调 GC 参数jstat 加 GC 日志是标配工具链。上一篇《JVM内存模型》 |下一篇《并发编程一volatilesynchronized》系列专栏《Java 后端核心知识图谱》Java专栏聊聊你的经历生产环境你们用哪个 GC我们 CMS→G1STW 从 2 秒降到 200ms但调参调了整整一周——G1 的 MaxGCPauseMillis 设太小反而吞吐量暴跌。你们 GC 调参踩过什么坑如果这份 GC 选型速查表帮你下次选 GC 时不再纠结欢迎收藏点赞