谈谈垃圾回收器的演进-从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 之后,回收器逐步向低延迟、灵活性方向发展。

相关推荐
lgily-12255 分钟前
常用的设计模式详解
java·后端·python·设计模式
意倾城1 小时前
Spring Boot 配置文件敏感信息加密:Jasypt 实战
java·spring boot·后端
火皇4051 小时前
Spring Boot 使用 OSHI 实现系统运行状态监控接口
java·spring boot·后端
薯条不要番茄酱1 小时前
【SpringBoot】从零开始全面解析Spring MVC (一)
java·spring boot·后端
懵逼的小黑子9 小时前
Django 项目的 models 目录中,__init__.py 文件的作用
后端·python·django
小林学习编程10 小时前
SpringBoot校园失物招领信息平台
java·spring boot·后端
java1234_小锋12 小时前
Spring Bean有哪几种配置方式?
java·后端·spring
柯南二号13 小时前
【后端】SpringBoot用CORS解决无法跨域访问的问题
java·spring boot·后端
每天一个秃顶小技巧14 小时前
02.Golang 切片(slice)源码分析(一、定义与基础操作实现)
开发语言·后端·python·golang
gCode Teacher 格码致知15 小时前
《Asp.net Mvc 网站开发》复习试题
后端·asp.net·mvc