----------- 先赞后看 👍 效果翻倍 🔥 ----------------
在开始之前,必须再次强调 “不可能三角”:内存占用、吞吐量、延迟,三者难以同时完美。
传统的垃圾收集器(如 Serial, Parallel, CMS, G1)在堆内存变大时,停顿时间(Latency)也会显著变长,因为它们总有一些阶段需要“Stop The World”(挂起所有用户线程)来完成清理工作。这对于需要快速响应的应用(如实时交易、大数据平台、微服务)是无法接受的。
Shenandoah 和 ZGC 的目标就是打破这个魔咒,实现在任何堆大小下(比如 4TB),停顿时间都能被严格控制在十毫秒以内,同时尽可能减少对吞吐量的影响。
它们实现这一目标的核心理念是:将最耗时的“对象移动”阶段也并发化。这听起来简单,但实现起来极其复杂,因为在你移动对象的同时,用户线程可能正在读写它。两者解决方案不同,但都极其精妙。
Shenandoah: “搬家队长”与“转发牌”
你可以把 Shenandoah 想象成一个高效的搬家队长,它的核心绝招是 “转发指针”(Brooks Pointer)。
1. 它是谁?
- 由 RedHat 开发,是 OpenJDK 的“养子”,OracleJDK 中不包含它。
- 你可以把它看作是 G1 的激进并发版,架构和 G1 很像(基于 Region 的堆布局),共享部分代码。
2. 它如何实现并发搬家?(核心原理)
想象一下,你要给一个正在营业的超市换货架,顾客还在不停买东西。你怎么做?
Shenandoah 的解决方案是:给每个商品(对象)挂上一个“转发牌”。
-
准备工作:在每个对象内部,额外开辟一个小空间(在对象头前),里面存着一个地址。正常情况下,这个地址指向对象自己,就像商品上挂着一个写着“我在这里”的牌子。
-
开始搬家(并发回收阶段):Shenandoah 启动一个并发线程,悄悄地把需要回收的区域(Region)里的存活对象复制到新区域。这整个过程是不停止营业(用户线程)的。
-
更新地址牌:对象复制完成后,Shenandoah 只做一件事:修改旧对象上的那个“转发牌”,把上面的地址从“旧对象”改为“新对象”的地址。注意:它不会去更新所有指向这个旧对象的引用。
-
顾客访问(读/写屏障):这就是关键所在!Shenandoah 在 JVM 中设置了“哨兵”(读屏障和写屏障)。每当用户线程(顾客)要访问一个对象时,哨兵会先拦截这次访问,检查一下这个对象上的“转发牌”。
- 如果牌子指向自己:说明没搬过家,直接访问。
- 如果牌子指向别处:说明这个对象是旧对象,已经搬走了。哨兵会自动地、悄悄地根据牌子上的新地址,去访问新对象,并把这次访问的结果返回给用户。同时,它还会顺手把这个引用本身的值更新成新地址(只有第一次需要转发,后续就直接访问新对象了)。这个过程对用户线程是完全透明的。
为什么能控制停顿?
因为最耗时的“复制对象”和“更新引用”工作都被并发线程和“哨兵”(屏障)分担了。那些必需的短暂停顿(初始标记、最终标记等)只处理少量核心信息(GC Roots),与堆大小无关,所以非常短。
3. 优缺点
- 优点:停顿时间极短,与堆大小脱钩。
- 缺点:“哨兵”(尤其是读屏障)带来的开销非常大。因为每一次对象读取操作都要经过这个检查步骤。这导致 Shenandoah 的吞吐量损失通常是三者中最大的。
ZGC: “魔法指针”与“自愈能力”
ZGC 的思路更加科幻。它不像 Shenandoah 那样给对象挂牌子,而是直接给指针(内存地址)施了魔法,它的核心是 染色指针(Colored Pointer)。
1. 它是谁?
- 由 Oracle 亲儿子开发,血统纯正,OpenJDK 和 OracleJDK 都包含。
- 它的设计理念源自传说中的 Azul C4 收集器,非常前沿。
2. 它如何实现并发搬家?(核心原理)
继续用超市搬家的比喻。ZGC 的做法不是挂牌子,而是它有一种“魔法墨水”,可以直接在写有商品位置的导购图(指针)上做标记。
-
魔法墨水:在 64 位系统中,我们其实用不了那么大的地址空间。ZGC 巧妙地利用了指针中未使用的比特位(比如高 4 位)来存储信息。这些信息包括:对象是否被标记、是否属于待回收集合、是否已被移动过。
所以,ZGC 的指针不仅仅是地址,它本身就是携带元数据的。通过这个指针,ZGC 不用访问对象就能知道它的状态。 -
地址重映射(魔法地图):光在指针上写墨水,CPU 可不认账,它会把这些位也当成地址的一部分,会找错地方。ZGC 的解决方案是 多重映射:它通过操作系统的内存管理功能,将好几块不同的虚拟内存地址空间(比如,指针标志位是 0010 的地图和 0000 的地图)都映射到同一块真实的物理内存上。这样,无论指针上的“魔法墨水”怎么写,最终都能通过这张“魔法地图”找到正确的物理对象。
-
并发搬家与自愈:当 ZGC 要移动一个对象时:
- 它并发地将对象复制到新 Region。
- 它在旧对象的位置上留下一个“转发地址”(类似于转发表)。
- 最关键的一步来了:当用户线程试图访问一个已经被移动的旧对象时,ZGC 的“哨兵”(读屏障)会被触发。这个哨兵一看指针上的“魔法墨水”(标志位),就知道“哦,这个对象搬走了”。于是它:
- 去旧位置上的“转发地址”里找到新地址。
- 直接把这个线程手中的指针值更新成新地址(并修正标志位)!
- 再去新地址访问对象。
这个过程被称为 “自愈”(Self-Healing)。这次访问之后,这个引用本身就已经被修正了,下次再访问就是直接访问新对象,没有任何额外开销。 这是它与 Shenandoah 每次访问都可能需要检查的关键区别。
3. 优缺点
- 优点:
- 停顿时间极短,同样与堆大小无关。
- “自愈”能力使得运行时开销远小于 Shenandoah,因此吞吐量表现通常比 Shenandoah 好得多,甚至接近 G1。
- 无需像 G1 那样维护记忆集,节省了内存。
- 缺点:
- 实现极其复杂,严重依赖底层操作系统特性(如多重映射)。
- 不支持分代收集(目前),可能导致浮动垃圾较多,抗突发流量能力稍弱。不过这是工程选择,并非技术不能实现。
总结与对比:你该选谁?
特性 | Shenandoah | ZGC |
---|---|---|
核心原理 | 转发指针 (Brooks Pointer) | 染色指针 (Colored Pointer) |
实现方式 | 在对象头前加额外指针 | 利用指针的未使用位存储元数据 |
关键机制 | 读屏障 + 写屏障 | 读屏障(目前无需写屏障) |
引用更新 | 每次访问都可能需要转发检查 | “自愈”,仅第一次访问慢 |
性能特点 | 低延迟,但吞吐量损失较大 | 低延迟,吞吐量损失很小 |
血缘关系 | 像 G1 的并发升级版 | 像 Azul C4 的 OpenJDK 实现 |
支持现状 | 仅 OpenJDK 包含 | OpenJDK 和 OracleJDK 都包含 |
如何选择?
- 追求极致低延迟,且运行在 OpenJDK 上:两者都是绝佳选择。
- 同时非常关心吞吐量性能:优先尝试 ZGC,它的性能表现通常更均衡。
- 需要使用 OracleJDK 并获得商业支持:只能选择 ZGC。
- 应用分配速率极高,堆内存巨大:目前两者都可能因为不分代而面临浮动垃圾的压力,需要预留足够堆内存。这是所有不分代收集器的共性问题。
总而言之,Shenandoah 和 ZGC 都代表了 JVM 垃圾收集技术的最高水平,它们通过不同的魔法将延迟降低到了前人无法想象的程度。ZGC 凭借其“魔法指针”和“自愈”能力,在实现上更显优雅,性能开销也更小,是目前更受瞩目的未来之星。