Featured image of post 备战JDK25--垃圾回收器

备战JDK25--垃圾回收器

JDK25今年年底就要发布了,这次深入了解结构化并发和分代Shenandoah GC垃圾回收器

阅读本文前,强烈建议先看看我之前的关于各种垃圾回收的介绍文章

垃圾回收部分

由于JDK25会将Shenandoah最为默认垃圾回收器,本次将再次详细研究一下本时代的垃圾回收器。

本次只提目前仍可能存活在市场上的三种垃圾回收器(G1,ZGC,Shenandoah GC)

G1(高吞吐量)

现在看来G1已经是老古董了,当初的出现只是为了取代CMS。但后续两种垃圾回收器都是基于G1的思想来做的。

简介

从JDK9开始,成为默认的垃圾回收器,低延迟、高吞吐,适用于10G以上的大内存。只优先回收垃圾最多的区,因此叫做Garbage-First

引入了区(Region)的概念,区在创建时没有定义属性。在程序的运行过程中,会被分成不同的类型。

  • 区分类
区类型作用
Eden Regions存放新创建的对象
Survivor Regions存放从 Eden 晋升的对象
Old Regions存放长期存活的对象
Humongous Regions存放大对象(超过 Region 一半大小)
Free Regions未被使用、等待分配

垃圾回收过程

2023-2-2319_13_12-1677150791924.png

  1. 初始标记(Short STW)

标记所有可以直接从GC Roots可达的对象。此阶段只对新生代Eden区进行标记。

  1. 并发标记(与用户线程并行)

从初始标记的结果出发,遍历整个堆,并标记所有可达对象。

创建一份全堆的对象存活快照。

  1. 重新标记(也叫最终标记,较短的STW)

纠正并发标记期间因修改产生的不一致。确保垃圾回收器有准确的存活集信息。

  1. 混合回收(并行)

完整的回收(年轻代和老年代都回收)。调度器会选择垃圾最多的区来进行清理。

  1. 清理

重新构建各区块的元素,更新各区的分类,更新记忆集,管理跨区之间的引用。

b47f048fb6844ecc90b80aff88117a04.png

特性

  • 卡表

用来记录哪些区发生了修改。每个区都有对应的卡表,当需要写入对象时,写入屏障会把对应的卡表标记为脏,GC遍历时只需要遍历脏卡表。

但是会占用额外的空间,而且写操作受到了影响。

  • 记忆集

在区之间进行回收时,记忆集用来记录跨区的引用,这样每个区只需要扫描局部被修改的部分。跨区部分信息记录在记忆集中。

也会占用额外的空间

  • 动态调整区类型

动态的调整区类型,来避免full GC

对比

G1是在垃圾回收效率和停顿延迟上取平衡的回收器。

缺点

  • 垃圾回收仍然STW

当堆内存特别大时(上T级别),初始标记和remark阶段的STW会非常明显。

ZGC和Shenandoah更优

  • 大内存支持不好(T级别内存)

  • 回收调度和标记开销

由于在写入时会向卡表写入数据,且还维护着记忆集,都会带来大量内存开销和算法开销。

  • 调优难

由于可以预先设置区大小等,且区是动态变化的,各方面想找到平衡难。

ZGC(低延迟)

又一个CMS,一直在优化,一直很nb,但没有当过真正的默认垃圾回收器

JDK11被引入。主打极低的停顿时间(一般停顿时间都小于10毫米)

同时支持多TB级别的堆内存

完全取消了年轻代老年代的划分,直接根据分区来管理内存。

垃圾回收过程

c84674f0f2f94702af64b7dd51f99e1d.png

  1. 初始标记(极短的STW)

扫描所有GC Roots可达对象

只处理直接引用,工作十分轻量,STW时间非常短

  1. 并发标记

在初始标记的基础上,并发遍历整个堆,从根出发标记所有可达的存活对象

  • load barrier协议来准确的标记对象状态

通过 load barrier的协议,使得对象状态可以被准确的确定,同时不影响应用程序的执行

  1. 并发迁移

根据标记的结果,将存活对象从原区域搬迁到新的区域

  • 通过虚拟内存地址更换迁移

利用虚拟内存重映射技术,通过调整对象的虚拟地址完成迁移,不需要做大量的复制

  • 彩色指针来实现引用升级

利用加载屏障和彩色指针,实现用户线程加载对象时,判断彩色指针去迁移后的地址读取对象,同时更新对象的引用

  1. 最终更新/修正阶段(Final Update)STW

快速修改遗漏或不一致的引用,确保整个堆中所有对象引用都正确的指向了新地址

特性

彩色指针

64位操作系统中的对象空间是64位,但实际对象的有效地址位数远低于64位。ZGC利用未使用的高位,将部分位“涂色”,用来存储额外的元数据信息。

  • 避免额外的数据结构

与G1标记对象是否可以回收(通过卡表)不同,不需要占用额外的空间,且不需要进行多余的随机读写,直接通过对象的地址来判断对象是否有效。

加载屏障(load barrier)

在对象引用加载时,自动插入一段代码(有点类似于AOP的概念)

  • 动态的检测颜色

    判断上面的彩色指针位的颜色,来判断对象是否被移动。

  • 透明指针重定向

    检测到对象已迁移,主动修改元数据中的新地址(新地址通过内部的转发表或基于虚拟内存映射的信息来确认)。并加载重定位的新地址。

  • 并发容忍性

    通过上面两项,可以保证用户线程看到的总是最新且正确的对象引用。从而实现了无暂停的迁移与回收。

虚拟内存重映射(同时也是CPU零拷贝的重要手段之一)

ZGC降低复制开销的主要手段

  • 通过修改操作数据页表项,来改变虚拟地址与物理内存之间的映射关系。

对比

垃圾回收器目标实现手段
ZGC极低停顿(10毫秒)
支持大内存(TB)超级大内存支持情况比Shenandoah好
并发执行
彩色指针
加载屏障
虚拟内存映射
G1均衡的吞吐量与停顿时间
可以通过修改最大停顿时间参数来动态分配区
利用区来划分堆
按垃圾率决定优先回收的区
采用复制算法减少内存碎片
部分场景STW,但STW时间可预测
Shenandoah极低停顿时间(几毫秒内)
支持大堆内存
与ZGC类似,并发压缩
大部分垃圾回收工作与应用内存并发执行
优点
  • 极低暂停时间(10毫秒)
  • 高扩展性:可以适配超大堆,停顿时间与堆大小无关
  • 并发对象搬迁与透明更新:极大的降低了实时数据迁移产生不一致的风险
  • 较低的内存复制开销:利用虚拟内存映射减少内存复制
缺点
  • 加载屏障需要额外开销:每次对象被访问时,都需要通过加载屏障,会带来额外的开销
  • 平台支持有限:ZGC主要是为64为Linux操作系统设计,对其他平台支持不成熟
  • 吞吐量有限:由于设计之初是为了更低的停顿时间,因此在极端吞吐量的场景下,效果不如传统垃圾回收器(Parallel GC)

ZGC为了提高CPU利用率而放弃了部分吞吐量

  • 为什么放弃ZGC?

个人认为,ZGC的延迟和大堆表现真的很棒。但大部分企业根据没有几十TB的堆。且ZGC需要太多的CPU开销,牺牲了部分的吞吐量(相当于没有高效的利用内存空间)这些部份甚至开了倒车。

总结就是有点偏科,且擅长的方向现实生活还没有跟上。

Shenandoah(非分代)

JDK12开始引入(社区可以支持到JDK8),通用采取分区概念,全堆并发标记、并发搬迁、并发更新引用。

只有几毫秒的STW,不区分新生代、老年代。所有对象都是同一套并发处理流程。

垃圾回收过程

1751436527413.png

  1. 初始标记(STW)

    扫描GC Roots中直接引用的对象

  2. 并发标记

    遍历整个堆,从根对象出发将所有可达对象标记出来。

    此过程中类似G1,使用了卡表、记忆集技术,来限制每次扫描的范围。避免出现全堆扫描

  3. 重新标记(STW)

    几毫秒的STW,重新扫描并发标记中的引用变化

  4. 并发整理/搬迁

    和ZGC类似,采用“彩色指针”、“加载屏障”、“虚拟内存映射”技术来实现并发的对象整理

  5. 最终引用更新(STW)

    更新上一步中未更新的引用

对比

非分代Shenandoah和ZGC大部分场景都是相同的,不同点很少

阶段ZGC非分代Shenandoah
并发标记利用加载屏障,实现标记过程中实时捕获对象状态,在超大堆上支持更好(TB)利用卡表、记忆集来并发标记,最后有短暂的暂停(再标记阶段),支持上百G的内存
迁移过程利用虚拟内存映射,强调零拷贝,几乎无复制虽然同样采用了彩色指针、加载屏障来保证迁移的准确,但还是存在部分的实际对象复制,所以效率不如ZGC

1751436709608.png

  • 为什么选择Shenandoah而不是ZGC?

从上面的比较感觉,好像ZGC要比Shenandoah更好,但这只是部份场景的比较。

选择Shenandoah主要原因是CPU开销成熟的应用经验,ZGC强调的零拷贝,但可能带来更大的CPU开销。

分代Shenandoah

JDK21引入,又重新引入了分代思想,利用对象年轻即死的假设,把堆重新分成了新生代和老年代。不同代采用不同的回收策略

新生代GC(Minor GC)

  1. 分配

    对象首先在新生代Eden区分配。由于新生代区域小、生命周期短,分配速度块。

  2. 复制收集

    当Eden区达到一定阈值,会触发STW,针对新生代单独回收。

    回收方式采用复制法,把存活的对象复制到幸存者区(Survivor),如果达到一定标准,会直接晋升到老年代。

    复制过程采用加载屏障,保证数据不会在搬迁过程中不可用。

  • 晋升标准

普通对象晋升到幸存者区。

如果幸存者区内存不足时,就会直接晋升到老年代。

老年代GC(Major GC)

老年代GC是全堆的GC

  1. 初始标记(STW,非常短):类似非分代模式,标记GC Roots直接可达的对象。
  2. 并发标记:然后多个线程遍历整个老年代。利用记忆集和卡表来减少扫描范围。
  3. 并发整理(搬迁):把存活的对象搬迁到连续的区域。利用彩色指针记录搬迁的信息,利用加载屏障并行搬迁。(有可能会用到虚拟内存映射)
  4. 最终更新(STW):对未完全更新的引用做最后的修正,保证全堆一致。

特点

  • 新生代的轻量复制

所谓的轻量复制,只新生代空间小。GC频繁,且只复制存活的对象。复制过程中有加载屏障。只会在初始标记GC Roots阶段产生短暂的STW。

这样提高GC频率,从而快速回收大部分垃圾。

  • 通过记忆集实现跨代引用

由于存在分代晋升,当新生代对象进入老年代时,回收规则发生变化。

通过记忆集来传递这样的引用信息。每次进入老年代更新记忆集。这样就不用每次扫描整个堆来确认老年代新增的对象。

  • 为什么选择分代 Shenandoah GC

目前看来,分代 Shenandoah GC 是垃圾回收效率,STW时间,吞吐量上保证了平衡。

新生代采用”G1 PRO“的方式来保证大部分垃圾可以被快速的回收。(吞吐量)

老年代采用传统Shenandoah来保证大内存(小于等于TB)的STW时间缩短。属于低延迟和高吞吐的融合。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计