谈谈垃圾回收器的演进-从SerialGC到G1、ZGC、Shenandoah

1. 垃圾回收器的演进历史

垃圾回收技术随着编程语言和硬件的发展逐步演进。最早的垃圾回收可以追溯到 1959 年 John McCarthy 为 Lisp 语言设计的简单标记-清除(Mark-Sweep)算法。随着 Java 等现代语言的兴起,垃圾回收器变得更加复杂和高效。以下是 Java 中垃圾回收器的简要演进历史:

  • Serial GC(串行垃圾回收器):Java 早期版本的默认回收器,单线程运行,适合单核 CPU 和小内存场景。
  • Parallel GC(并行垃圾回收器):Java 5 引入,利用多线程并行回收,目标是提高吞吐量,适合多核 CPU。
  • CMS(Concurrent Mark-Sweep):Java 5 引入的并发回收器,目标是降低停顿时间(低延迟),通过与应用线程并发执行部分回收工作实现。
  • G1(Garbage First):Java 7 正式引入(Java 9 默认),目标是平衡吞吐量和低延迟,采用区域化(Region-based)内存管理,逐步取代 CMS。
  • ZGC 和 Shenandoah:Java 11 和 12 引入的超低延迟回收器,停顿时间缩短到毫秒级,适合大规模、低延迟应用。

演进的驱动力主要是硬件(从单核到多核)、应用需求(吞吐量 vs. 低延迟)和内存管理复杂性的增加。

2. 分代回收:年轻代和老年代

垃圾回收器分为年轻代(Young Generation)和老年代(Old Generation),这是 Java 中分代回收(Generational GC)的核心思想。分代回收基于"弱分代假说":

  • 大多数对象很快变得不可达(短生命周期)。
  • 老对象很少引用新对象。

基于此,Java 堆被划分为:

  • 年轻代:包括 Eden 区和两个 Survivor 区(S0 和 S1),用于存放新创建的对象。年轻代使用"复制算法"(Copying),因为对象存活率低,复制少量存活对象效率高。
  • 老年代:存放经过多次 GC 仍存活的对象,通常使用"标记-清除"或"标记-整理"算法,因为存活对象较多。

在 G1 之前,Java 的主流垃圾回收器(如 Serial、Parallel、CMS)都采用分代回收设计。G1 虽然也支持分代,但它引入了"区域"(Region)的概念,不再严格区分连续的年轻代和老年代,而是动态分配区域的角色(Eden、Survivor、Old),更灵活。

3. 垃圾回收算法

垃圾回收算法是垃圾回收器的实现基础,主要有以下几种:

(1) 标记-清除(Mark-Sweep)

  • 过程:先标记存活对象,然后清除未标记的对象。
  • 优点:实现简单,不移动对象。
  • 缺点:产生内存碎片(不连续的空闲内存块)。
  • 碎片处理:标记-清除本身不处理碎片。如果碎片过多,可能触发"标记-整理"或 Full GC 来整理内存。CMS 回收器就使用标记-清除,碎片问题通过定期整理或用户手动触发 Full GC 解决。

(2) 标记-复制(Mark-Copy)

  • 过程:将内存分为两块(From 和 To),标记存活对象后,将其复制到另一块,清空原区域。
  • 优点:无碎片,适合存活对象少的场景。
  • 缺点:内存利用率低(一半空间闲置)。
  • 应用:年轻代的 Minor GC 常用,比如 Serial 和 Parallel GC 的年轻代实现。

(3) 标记-整理(Mark-Compact)

  • 过程:标记存活对象后,将其移动到内存一端,清除剩余部分。
  • 优点:无碎片,内存利用率高。
  • 缺点:移动对象开销大,停顿时间长。
  • 应用:老年代常用,比如 Parallel Old GC 和 CMS 的 Full GC 阶段。

4. 垃圾回收器与算法的对应关系

不同垃圾回收器组合使用上述算法:

  • Serial GC:年轻代用标记-复制,老年代用标记-整理。
  • Parallel GC:年轻代用标记-复制,老年代用标记-整理(Parallel Old)。
  • CMS:年轻代用标记-复制(ParNew),老年代用标记-清除(并发执行),碎片问题依赖 Full GC 解决。
  • G1:年轻代用标记-复制,老年代用混合回收(Mixed GC),结合标记-清除和局部整理,碎片通过区域管理缓解。
  • ZGC/Shenandoah:使用标记-整理,但通过并发移动对象实现超低停顿。

5. G1 之前都是分代回收吗?

是的,在 G1 之前,Java 的主流垃圾回收器(Serial、Parallel、CMS)都严格基于分代回收设计,分年轻代和老年代。G1 虽然保留了分代思想,但通过区域化管理打破了传统分代回收的固定内存布局。它将堆划分为多个小区域(Region),动态分配 Eden、Survivor 和 Old 角色,回收时优先处理"垃圾最多"的区域(Garbage First),因此更灵活。

6. 更具体的解释:以 CMS 为例

以 CMS(Concurrent Mark-Sweep)为例,详细说明其工作方式:

  • 年轻代:使用 ParNew(并行标记-复制),多线程复制存活对象到 Survivor 或老年代。
  • 老年代
    1. 初始标记(STW):标记 GC Roots 可达对象,停顿时间短。
    2. 并发标记:与应用线程并发,标记所有存活对象。
    3. 重新标记(STW):修正并发标记中的遗漏,停顿时间稍长。
    4. 并发清除:清除未标记对象,产生碎片。
  • 碎片处理:CMS 不主动整理碎片。如果碎片导致分配失败,会触发 Full GC(单线程标记-整理),停顿时间显著增加。

总结

垃圾回收器的设计和算法选择紧密相关,分代回收是 G1 之前的主流范式。标记-清除的碎片问题通过整理或区域管理解决,而 G1 之后,回收器逐步向低延迟、灵活性方向发展。

相关推荐
uhakadotcom30 分钟前
Thrift2: HBase 多语言访问的利器
后端·面试·github
Asthenia041231 分钟前
Java 类加载规则深度解析:从双亲委派到 JDBC 与 Tomcat 的突破
后端
方圆想当图灵42 分钟前
从 Java 到 Go:面向对象的巨人与云原生的轻骑兵
后端·代码规范
Moment42 分钟前
一份没有项目展示的简历,是怎样在面试里输掉的?开源项目或许是你的救命稻草 😭😭😭
前端·后端·面试
Asthenia04121 小时前
JavaSE Stream 是否线程安全?并行流又是什么?
后端
半部论语1 小时前
SpringMVC 中的DispatcherServlet生命周期是否受Spring IOC 容器管理
java·后端·spring
Asthenia04121 小时前
JavaSE-常见排序:Arrays/Collections/List/StreamAPI
后端
Asthenia04121 小时前
深入浅出分析JDK动态代理与CGLIB动态代理的区别
后端
追逐时光者2 小时前
C#/.NET/.NET Core技术前沿周刊 | 第 32 期(2025年3.24-3.31)
后端·.net
uhakadotcom2 小时前
轻松掌握XXL-JOB:分布式任务调度的利器
后端·面试·github