低延迟垃圾收集器:挑战“不可能三角”

----------- 先赞后看 👍 效果翻倍 🔥 ----------------

在开始之前,必须再次强调 "不可能三角"内存占用、吞吐量、延迟,三者难以同时完美。

传统的垃圾收集器(如 Serial, Parallel, CMS, G1)在堆内存变大时,停顿时间(Latency)也会显著变长,因为它们总有一些阶段需要"Stop The World"(挂起所有用户线程)来完成清理工作。这对于需要快速响应的应用(如实时交易、大数据平台、微服务)是无法接受的。

Shenandoah 和 ZGC 的目标就是打破这个魔咒 ,实现在任何堆大小下(比如 4TB),停顿时间都能被严格控制在十毫秒以内,同时尽可能减少对吞吐量的影响。

它们实现这一目标的核心理念是:将最耗时的"对象移动"阶段也并发化。这听起来简单,但实现起来极其复杂,因为在你移动对象的同时,用户线程可能正在读写它。两者解决方案不同,但都极其精妙。


Shenandoah: "搬家队长"与"转发牌"

你可以把 Shenandoah 想象成一个高效的搬家队长,它的核心绝招是 "转发指针"(Brooks Pointer)

1. 它是谁?

  • 由 RedHat 开发,是 OpenJDK 的"养子",OracleJDK 中不包含它
  • 你可以把它看作是 G1 的激进并发版,架构和 G1 很像(基于 Region 的堆布局),共享部分代码。

2. 它如何实现并发搬家?(核心原理)

想象一下,你要给一个正在营业的超市换货架,顾客还在不停买东西。你怎么做?

Shenandoah 的解决方案是:给每个商品(对象)挂上一个"转发牌"

  1. 准备工作:在每个对象内部,额外开辟一个小空间(在对象头前),里面存着一个地址。正常情况下,这个地址指向对象自己,就像商品上挂着一个写着"我在这里"的牌子。

  2. 开始搬家(并发回收阶段) :Shenandoah 启动一个并发线程,悄悄地把需要回收的区域(Region)里的存活对象复制到新区域。这整个过程是不停止营业(用户线程)的

  3. 更新地址牌 :对象复制完成后,Shenandoah 只做一件事 :修改旧对象 上的那个"转发牌",把上面的地址从"旧对象"改为"新对象"的地址。注意:它不会去更新所有指向这个旧对象的引用

  4. 顾客访问(读/写屏障) :这就是关键所在!Shenandoah 在 JVM 中设置了"哨兵"(读屏障和写屏障)。每当用户线程(顾客)要访问一个对象时,哨兵会先拦截这次访问,检查一下这个对象上的"转发牌"。

    • 如果牌子指向自己:说明没搬过家,直接访问。
    • 如果牌子指向别处 :说明这个对象是旧对象,已经搬走了。哨兵会自动地、悄悄地 根据牌子上的新地址,去访问新对象,并把这次访问的结果返回给用户。同时,它还会顺手把这个引用本身的值更新成新地址(只有第一次需要转发,后续就直接访问新对象了)。这个过程对用户线程是完全透明的。

为什么能控制停顿?

因为最耗时的"复制对象"和"更新引用"工作都被并发线程和"哨兵"(屏障)分担了。那些必需的短暂停顿(初始标记、最终标记等)只处理少量核心信息(GC Roots),与堆大小无关,所以非常短。

3. 优缺点

  • 优点:停顿时间极短,与堆大小脱钩。
  • 缺点"哨兵"(尤其是读屏障)带来的开销非常大 。因为每一次对象读取操作都要经过这个检查步骤。这导致 Shenandoah 的吞吐量损失通常是三者中最大的

ZGC: "魔法指针"与"自愈能力"

ZGC 的思路更加科幻。它不像 Shenandoah 那样给对象挂牌子,而是直接给指针(内存地址)施了魔法 ,它的核心是 染色指针(Colored Pointer)

1. 它是谁?

  • 由 Oracle 亲儿子开发,血统纯正,OpenJDK 和 OracleJDK 都包含。
  • 它的设计理念源自传说中的 Azul C4 收集器,非常前沿。

2. 它如何实现并发搬家?(核心原理)

继续用超市搬家的比喻。ZGC 的做法不是挂牌子,而是它有一种"魔法墨水",可以直接在写有商品位置的导购图(指针)上做标记

  1. 魔法墨水 :在 64 位系统中,我们其实用不了那么大的地址空间。ZGC 巧妙地利用了指针中未使用的比特位 (比如高 4 位)来存储信息。这些信息包括:对象是否被标记、是否属于待回收集合、是否已被移动过。

    所以,ZGC 的指针不仅仅是地址,它本身就是携带元数据的。通过这个指针,ZGC 不用访问对象就能知道它的状态。

  2. 地址重映射(魔法地图) :光在指针上写墨水,CPU 可不认账,它会把这些位也当成地址的一部分,会找错地方。ZGC 的解决方案是 多重映射 :它通过操作系统的内存管理功能,将好几块不同的虚拟内存地址空间(比如,指针标志位是 0010 的地图和 0000 的地图)都映射到同一块真实的物理内存上。这样,无论指针上的"魔法墨水"怎么写,最终都能通过这张"魔法地图"找到正确的物理对象。

  3. 并发搬家与自愈:当 ZGC 要移动一个对象时:

    • 它并发地将对象复制到新 Region。
    • 它在旧对象的位置上留下一个"转发地址"(类似于转发表)。
    • 最关键的一步来了 :当用户线程试图访问一个已经被移动的旧对象时,ZGC 的"哨兵"(读屏障 )会被触发。这个哨兵一看指针上的"魔法墨水"(标志位),就知道"哦,这个对象搬走了"。于是它:
      1. 去旧位置上的"转发地址"里找到新地址。
      2. 直接把这个线程手中的指针值更新成新地址(并修正标志位)!
      3. 再去新地址访问对象。

    这个过程被称为 "自愈"(Self-Healing)这次访问之后,这个引用本身就已经被修正了,下次再访问就是直接访问新对象,没有任何额外开销。 这是它与 Shenandoah 每次访问都可能需要检查的关键区别。

3. 优缺点

  • 优点
    1. 停顿时间极短,同样与堆大小无关。
    2. "自愈"能力使得运行时开销远小于 Shenandoah ,因此吞吐量表现通常比 Shenandoah 好得多,甚至接近 G1。
    3. 无需像 G1 那样维护记忆集,节省了内存。
  • 缺点
    1. 实现极其复杂,严重依赖底层操作系统特性(如多重映射)。
    2. 不支持分代收集(目前),可能导致浮动垃圾较多,抗突发流量能力稍弱。不过这是工程选择,并非技术不能实现。

总结与对比:你该选谁?

特性 Shenandoah ZGC
核心原理 转发指针 (Brooks Pointer) 染色指针 (Colored Pointer)
实现方式 在对象头前加额外指针 利用指针的未使用位存储元数据
关键机制 读屏障 + 写屏障 读屏障(目前无需写屏障)
引用更新 每次访问都可能需要转发检查 "自愈",仅第一次访问慢
性能特点 低延迟,但吞吐量损失较大 低延迟,吞吐量损失很小
血缘关系 G1 的并发升级版 Azul C4 的 OpenJDK 实现
支持现状 仅 OpenJDK 包含 OpenJDK OracleJDK 都包含

如何选择?

  • 追求极致低延迟,且运行在 OpenJDK 上:两者都是绝佳选择。
  • 同时非常关心吞吐量性能 :优先尝试 ZGC,它的性能表现通常更均衡。
  • 需要使用 OracleJDK 并获得商业支持 :只能选择 ZGC
  • 应用分配速率极高,堆内存巨大 :目前两者都可能因为不分代而面临浮动垃圾的压力,需要预留足够堆内存。这是所有不分代收集器的共性问题。

总而言之,Shenandoah 和 ZGC 都代表了 JVM 垃圾收集技术的最高水平,它们通过不同的魔法将延迟降低到了前人无法想象的程度。ZGC 凭借其"魔法指针"和"自愈"能力,在实现上更显优雅,性能开销也更小,是目前更受瞩目的未来之星。

相关推荐
佛祖让我来巡山1 天前
Java垃圾收集器全解:从Serial到G1的进化之旅
cms·gc·垃圾收集器·g1
boonya4 天前
Java垃圾回收机制理论算法及使用
jvm·算法·gc·垃圾收集器·理论
水中加点糖6 个月前
JVM-GC(G1)实践—GC异常定位、参数调整、GC更换
jvm·gc·jdk17·zgc·g1·gc定位·gc调优
张彦峰ZYF6 个月前
动态扩缩容引发的JVM堆内存震荡:从原理到实践的GC调优指南
jvm·cms·zgc·g1·zyf jvm 知识准备分享
小小小妮子~9 个月前
Java虚拟机——JVM高级特性与最佳实践
jvm·垃圾收集器··虚拟机类加载机制·算法.....
极客先躯9 个月前
高级java每日一道面试题-2024年12月07日-JVM篇-如何选择垃圾收集器?
java·jvm·zgc·g1 gc·serial gc·parallel gc·cms gc
威哥爱编程1 年前
Java Z 垃圾收集器如何彻底改变内存管理
java·jvm·zgc
木小同1 年前
JVM面试(六)垃圾收集器
java·jvm·面试·垃圾收集器·g1垃圾收集器
太阳伞下的阿呆1 年前
Jvm G1与ZGC启动参数
jvm·jdk·zgc·g1