Java 垃圾回收机制全景指南:从 GC 流程到现代收集器选型

张开发
2026/5/1 22:24:21 15 分钟阅读

分享文章

Java 垃圾回收机制全景指南:从 GC 流程到现代收集器选型
Java 垃圾回收机制全景指南从 GC 流程到现代收集器选型在 Java 的世界里内存管理是开发者最“幸福”的负担——我们无需手动free内存但必须理解 JVM 何时、如何回收内存。一旦理解不到位OOM (Out Of Memory)、STW (Stop-The-World)停顿过长、CPU 飙高等问题便会接踵而至。随着 JDK 版本的迭代特别是 JDK 11、17、21 的普及垃圾回收器GC发生了翻天覆地的变化。本文将深入剖析 GC 的核心原理、主流回收器的演变并提供基于场景的选型策略。一、核心基石如何判断对象已死在回收之前JVM 必须确定哪些对象是“垃圾”。主要有两种算法1. 引用计数法已废弃原理对象被引用一次计数器 1引用失效-1。为 0 时回收。缺陷无法解决循环引用问题对象 A 引用 BB 引用 A但两者都已不再被外部使用。现状Java不采用此方法。2. 可达性分析算法GC Roots Tracing原理从一组称为GC Roots的对象开始向下搜索。如果一个对象到 GC Roots 没有任何引用链相连则证明该对象不可用。常见的 GC Roots虚拟机栈栈帧中的本地变量表中引用的对象。方法区中类静态属性引用的对象。方法区中常量引用的对象。本地方法栈中 JNI即一般说的 Native 方法引用的对象。Java 虚拟机内部的引用如基本数据类型对应的 Class 对象。注意即使对象不可达也不会立即被回收至少要经历两次标记过程finalize()机制虽已不推荐但原理如此最终才会被清除。二、内存分代与回收策略为了优化效率JVM 将堆内存划分为不同的区域针对不同生命周期的对象采用不同的回收策略。1. 新生代 (Young Generation)特点对象朝生夕死存活率低。算法复制算法 (Copying)。将内存分为 Eden 区和两个 Survivor 区From/To。回收时将存活的对象复制到另一个 Survivor 区然后清空整个 Eden 和原 Survivor 区。优势无需整理碎片效率高只需扫描存活对象。触发条件Eden 区满时触发Minor GC(或 Young GC)。2. 老年代 (Old Generation)特点存放长期存活的对象存活率高。算法标记 - 清除 (Mark-Sweep)或标记 - 整理 (Mark-Compact)。标记 - 清除标记存活对象直接清除未标记的。缺点产生内存碎片。标记 - 整理标记存活对象将所有存活对象向一端移动清理边界外内存。优点无碎片但移动对象成本高。触发条件老年代空间不足或大对象直接进入老年代触发Major GC或Full GC。3. 元空间 (Metaspace) / 永久代 (PermGen)存储类信息、常量、静态变量等。JDK 8 之前叫永久代易溢出JDK 8 改为元空间使用本地内存默认不再限制大小可配置大大减少了java.lang.OutOfMemoryError: PermGen space错误。三、主流垃圾回收器深度对比JVM 提供了多种垃圾收集器每种都有其特定的设计目标。收集器代别算法线程模型特点适用场景状态Serial新生代/老年代复制/标记整理单线程简单高效但 STW 时间长客户端模式、单核 CPU、小内存 (100MB)保留Parallel Scavenge新生代复制多线程关注吞吐量(CPU 运行代码的时间占比)后台计算任务、大数据处理、对延迟不敏感默认 (JDK 8)Parallel Old老年代标记整理多线程配合 PS 使用关注吞吐量同上保留CMS(Concurrent Mark Sweep)老年代标记 - 清除并发关注低延迟首次实现并发收集互联网应用、B/S 架构、对响应时间敏感已废弃(JDK 9 废弃14 移除)G1(Garbage First)全堆 (Region)分区复制/整理并发 并行可预测的停顿时间兼顾吞吐与延迟通用首选大堆内存 (4GB)低延迟要求默认 (JDK 9)ZGC全堆染色指针 读屏障完全并发停顿时间 1ms支持 TB 级堆超低延迟系统、云原生、超大内存生产就绪 (JDK 15 正式17/21 增强)Shenandoah全堆类似 ZGC完全并发开源社区主导低停顿兼容旧版本同 ZGC常用于 OpenJDK 构建版生产就绪 (JDK 12)1. CMS曾经的王者时代的泪核心以获取最短回收停顿时间为目标。流程初始标记 (STW) - 并发标记 - 重新标记 (STW) - 并发清除。缺点基于标记 - 清除会产生内存碎片导致浮动垃圾过多时触发 Full GC。对 CPU 资源敏感并发阶段占用线程资源。结论在现代 JDK 中已被 G1 和 ZGC 取代新项目严禁使用。2. G1当前的绝对主力革新打破了物理上的新生代/老年代界限将堆划分为多个大小相等的Region。每个 Region 动态扮演新生代或老年代角色。核心思想优先回收价值最大的 RegionGarbage First从而在指定时间内-XX:MaxGCPauseMillis获得最高的回收效率。优势可预测的停顿模型。基于整理算法几乎无碎片。适合大内存服务器。现状JDK 9 及以后的默认收集器稳定且成熟。3. ZGC Shenandoah未来的标准革命性突破将耗时操作标记、整理全部移到与用户线程并发执行。核心技术染色指针 (Colored Pointers)(ZGC)将标记位存储在对象指针上而非对象头。读屏障 (Load Barriers)在读取对象引用时进行检查和修正。表现无论堆内存是 100MB 还是 16TB停顿时间都能控制在1 毫秒以内通常 0.5ms。建议如果你使用的是 JDK 17 或 JDK 21且对延迟极其敏感如高频交易、实时游戏服、即时通讯强烈建议直接使用 ZGC。四、GC 日志分析与调优思路1. 关键指标Throughput (吞吐量) (运行用户代码时间) / (运行用户代码时间 GC 时间)。越高越好。Pause Time (停顿时间)单次 GC 造成的系统暂停时长。越低越好。Frequency (频率)GC 发生的频繁程度。2. 常见调优参数 (以 G1 为例)# 指定最大停顿时间目标 (默认 200ms可根据业务调整如 50ms) -XX:MaxGCPauseMillis50 # 设置堆内存大小 (避免频繁扩容) -Xms4g -Xmx4g # 开启 GC 日志 (JDK 9) -Xlog:gc*:filegc.log:time,uptime,level,tags # 启用 ZGC (JDK 17) -XX:UseZGC3. 调优黄金法则不要过早优化先监控再调优。使用jstat,VisualVM,Arthas, 或云厂商的监控工具观察 GC 频率和停顿。优先升级 JDK新版本的 GC 算法通常比旧版本手动调优效果更好。例如从 JDK 8 (CMS/PS) 升级到 JDK 17 (G1/ZGC) 往往能直接解决大部分性能问题。大对象处理如果存在大量长生命周期的大对象考虑调整-XX:PretenureSizeThreshold让其直接进入老年代避免在新生代反复复制。内存泄漏排查如果 Full GC 后内存仍无法回收通常是代码层面的内存泄漏如静态集合无限增长、ThreadLocal 未移除此时调优参数无效需通过 Heap Dump 分析。五、选型决策树面对不同的业务场景如何选择 GC内存大小 100MB 或 单核 CPUSerial GC(简单粗暴无额外开销)。关注吞吐量 延迟(如离线批处理、数据清洗、科学计算)Parallel GC(JDK 8 默认)。它能最大化利用多核 CPU 进行垃圾回收哪怕停顿稍长也无所谓。通用场景 / 响应时间敏感 / 堆内存 4GB(如Web 服务、微服务、电商系统)G1 GC(JDK 9 默认)。它是目前最均衡的选择既能控制停顿又能保证高吞吐。配置建议设置-XX:MaxGCPauseMillis为目标值。超低延迟要求 / 超大堆内存 (TB 级)(如高频交易、实时风控、大型游戏服)ZGC(推荐 JDK 17/21)。理由它将停顿时间从“百毫秒级”降低到了“亚毫秒级”且几乎不需要复杂的参数调优。注意ZGC 会略微牺牲一点吞吐量约 10%-15%但在延迟敏感场景中这是值得的。六、总结与展望Java 的垃圾回收机制已经从早期的“手工坊”进化为高度自动化的“智能工厂”。过去开发者需要深入理解 CMS 的碎片问题精心计算分代大小痛苦地调优参数。现在随着G1的成熟和ZGC的普及大多数应用场景下“默认配置”已经是最佳实践。未来随着 Project Loom (虚拟线程) 的落地GC 与协程的协同将更加紧密停顿时间将进一步被感知弱化。给开发者的最终建议拥抱新版本尽可能使用 LTS 版本JDK 17 或 21享受最新的 GC 红利。首选 ZGC/G1除非有极特殊的遗留约束否则不要再使用 CMS 或 Parallel Old。监控先行建立完善的 GC 监控告警体系让数据指导优化而不是凭感觉猜测。理解 GC 不仅仅是为了面试更是为了在系统出现卡顿或崩溃时能够透过现象看本质快速定位并解决问题。

更多文章