Shenandoah的故事:简史

在我们深入探讨之前,让我们快速回顾一下历史。Shenandoah GC以Shenandoah山谷命名(令人失望的是,并不是以一个收集垃圾的精灵森林命名),在JDK 12中作为实验性功能引入。快进到Java 21,它现在是一个完全支持的、可用于生产环境的垃圾收集器。

Shenandoah的独特之处是什么?

Shenandoah的核心设计是解决“大堆问题”——无论堆的大小如何,最小化GC暂停时间。那么它与G1和ZGC有何不同呢?

  • 并发压缩:虽然G1在“停止世界”暂停期间进行压缩,Shenandoah则是并发进行的。
  • Brooks指针:与ZGC的彩色指针不同,Shenandoah使用Brooks指针进行对象重定位。
  • 撤离式压缩:对象被移动到新的内存位置,而不是在现有区域内滑动。

深入Shenandoah的内部

Brooks指针技术

Shenandoah的魔力在于Brooks指针。堆中的每个对象都包含一个额外的字——Brooks指针,最初指向对象本身。当对象被移动时,只需更新这个指针,其他引用保持不变。


class ShenandoahObject {
    Object forwardingPointer; // Brooks指针
    // 实际对象数据在后面
}

这个巧妙的技巧允许Shenandoah在不停止世界的情况下并发移动对象。

并发压缩舞蹈

Shenandoah的压缩过程是一场精心编排的线程舞蹈。以下是简化版的步骤:

  1. 标记:并发识别活动对象。
  2. 撤离:将活动对象复制到新位置,更新Brooks指针。
  3. 更新引用:扫描堆以更新对移动对象的引用。
  4. 清理:回收被撤离区域的内存。

所有这些都在您的应用程序线程仍在运行时发生。令人惊叹,对吧?

Shenandoah vs. G1 vs. ZGC:对决

让我们来看看Shenandoah与其竞争对手的比较:

特性 Shenandoah G1 ZGC
并发压缩
暂停时间 非常低 非常低
内存开销 中等
大堆性能 优秀 良好 优秀

实际调优:最小化暂停时间

现在,让我们动手实践一些Shenandoah的调优技巧:

1. 堆大小重要(但没那么重要)

使用Shenandoah,您可以更慷慨地设置堆大小,而不必担心长时间的GC暂停。开始使用:


java -XX:+UseShenandoahGC -Xms16G -Xmx16G YourApp

2. 调整分配阈值

调整Shenandoah触发GC周期的积极性:


-XX:ShenandoahAllocationThreshold=10

较低的值(如10%)会触发更频繁但更短的GC周期。

3. 采用自适应启发式

让Shenandoah适应您的应用程序行为:


-XX:ShenandoahGCHeuristics=adaptive

4. 监控和调整

使用JConsole或VisualVM等工具监控GC行为,不要害怕尝试不同的设置。

注意事项(因为总有一些注意事项)

在您急于重写所有JVM参数之前,请记住:

  • Shenandoah用CPU周期换取更低的暂停时间。在CPU密集型应用中,这可能会影响整体吞吐量。
  • Brooks指针技术会略微增加内存使用。
  • 虽然很少见,但如果GC无法跟上分配速度,您可能会遇到“分配失败”的问题。

总结:Shenandoah适合您吗?

Shenandoah在需要一致低延迟的场景中表现最佳,尤其是大堆情况下。如果您的应用程序符合以下任何一种情况,Shenandoah可能是您的新朋友:

  • 对延迟敏感的服务(例如,金融交易、游戏服务器)
  • 具有大型内存数据集的应用程序
  • 需要无论堆大小如何都能提供可预测响应时间的系统

思考的食粮

“最好的GC暂停是从未发生的。” - 匿名Java开发者(可能)

虽然Shenandoah令人印象深刻,但请记住,最终目标是编写高效、内存意识的代码。无论多么先进的垃圾收集器,都无法完全弥补优化不佳的应用程序。

接下来是什么?

在您开始Shenandoah之旅时,这里有一些资源供您参考:

请记住,GC的世界是不断发展的。保持好奇,继续实验,愿您的暂停时间永远对您有利!

您在Java 21项目中尝试过Shenandoah吗?在下面留言分享您的经验或您遇到的任何令人费解的GC难题。祝您垃圾收集愉快!