JVM 垃圾回收器 Shenandoah GC 的最新实践案例

。吞吐量的降低是可预测的,而且很容易做出应对计划。例如,如果你发现应用程序的运行速度慢了 10%,那就增加 10% 的服务器。而 GC 停顿发生得非常迅速,你无法针对它们进行“自动伸缩”,你能做的是为它们分配额外的资源,这些资源在大部分时间是闲置的,造成了金钱的浪费。

闲扯了这么多,接下来让我来介绍一下我在真实项目中使用 Shenandoah 的经验。

3初次相遇

先让我介绍一下这个应用程序,它实际上是一个反向代理,会对请求做一些预处理和后处理操作。代理对进入的请求稍作修改,把它们发给多个上游服务器,在收到来自上游的响应后,对响应进行合并,然后返回给客户端。这个看似简单的项目实际上有点复杂,因为请求和响应里会带有大量的 JSON,而且我们要求每秒处理 1 万个请求,网络带宽达到了每秒 350MB。我们使用了 AWS c5.9xlarge 实例,设置了 57GB 内存。应用程序本身不需要消耗多大内存,但它需要有足够的内存来暂存等待上游响应(最长响应时间为 5 秒钟)的请求。

刚开始我们使用的是 G1,新创建的服务在达到负载峰值之前可以正常运行,但在达到负载峰值后就变得非常脆弱。时而会出现几秒钟的 FullGC,并间接性地出现 100 毫秒到 200 毫秒的停顿。一个预期每秒可以处理 1 万个请求的服务在耗费 70% 资源处理负载时伴随着 5 秒钟的停顿,这种情况你能想象吗?很多请求被积压起来,在停顿之后的数秒内,它就像抽了疯一样。停顿期间和停顿之后被挂起的请求造成了 QoS 的降级。

在一开始,调整 G1 选项似乎对我们有点帮助,但后来反而变得更加不稳定。最直接的办法是调整年轻代和老年代比例,但这么做让应用程序出现奇怪的故障。我得承认自己并不是一个 GC 专家,我的方法可能有点稚嫩,但对于一个 Java 应用程序开发人员来说,你也别指望我对 GC 有多么深入的了解。

经过一些无效的尝试之后,我们切换到了可以使用 Shenandoah 的 OpenJDK 8 镜像(shipilev/openjdk-shenandoah:8),并在命令行参数中加入 -XX:+UseShenandoahGC,然后就出现了下面的这种情况:

图片

图中显示的是最大 GC 停顿的变化情况。Shenandoah 将“正常”的最大停顿从 50-150 毫秒减少到了 10-20 毫秒,而且图中并没有显示使用 G1 时常会出现的数秒钟的停顿。

突然间,服务的表现非常稳定。在解决了这些性能瓶颈之后,我们将每台机器的吞吐量又提升了一些。我们将堆大小设置到了 57GB,即使堆大了很多,但延迟并没有因此而增加。有了更大的堆缓冲区,我们就可以处理更大的流量高峰。总的 QoS 也得到了改进,并在更长的时间跨度内减少了延迟百分位。

4在 Shenandoah 的日子里

新垃圾回收器给我们带来的好日子持续了一段时间。虽然只是切换了垃圾回收器,但它在应用程序运行时方面带来的改进对我们来说是个巨大的胜利。不过,如果你对服务的性能和稳定性要求很苛刻,只是简单地修改一两个参数是不够的。接下来,我将进一步介绍这个回收器以及如何更好地使用它。

jvm-hiccup-meter

jvm-hiccup-meter 是一个小型的工具库,用来度量系统的停顿时间。它是 jHiccup 的极小化版本。jHiccup 用来累计程序运行整个过程的停顿时间,而 jvm-hiccup-meter 则通过回调持续地报告系统的停顿。

因为 Shenandoah(或者其它 Java GC www.meimeitu8.com也是)已经通过 MBean 和 GC 日志告诉我们有关 GC 停顿的信息,所以这个库似乎有点多余。但是,在有些时候,它可以报告可能被 GC 漏掉的停顿,或者其他与 GC 无关的停顿(例如在进行堆转储时)。

这个所谓的库只是一个简单的 Java 类,如果你不想在项目中引入新的依赖,可以直接将这个类拷贝到项目中。

5jvm-alloc-rate-meter

包括 Shenandoah 在内的大多数现代 GC 都可以轻松自如地处理大量的垃圾,但对于并发回收器来说,它们回收垃圾的速度需要比应用程序生成垃圾的速度更快。因此,如果能够知道应用程序生成对象的速度就好了。

可惜的是,JVM 并没有为我们提供这种方式。我们可以从 GC 日志中获取一些信息,但并不能用来进行实时监控。不过,我们可以使用另一个叫作 jvm-alloc-rate-meter 的库,用它来度量虚拟机分配内存的速率,并将这些数据发给监控系统。通过持续地观察这些指标,我们就可以直观地知道应用程序是不是分配了太多内存,这样就可以检测到可能会导致长 GC 停顿的峰值。

这个库也只是一个 Java 类,也可以直接拷贝到项目中。

内存分配分析器

知道内存分配速率固然很有用,但如果我们想知道什么时候该减少应用程序产生的垃圾,内存分析器似乎会更有用。它会告诉我们应用程序的哪些部分产生了最多的垃圾,然后我们就可以针对这些部分进行优化。

这类分析器有很多,我们最后选择的是 async-profiler 。async-profiler 使用了非侵入式的方式,所以可能不会非常准确,但因为开销非常小,可以被用在生产环境中。另外,async-profiler 生成的图表很容易看懂。

Shenandoah 的故障模式

即使 Shenandoah 很强大,拥有创新的设计,但它并不是一道魔法——它也只是一款运行在这个纷繁世界中的软件而已。所以,在某些特定条件下,它无法达到所宣称的停顿。因为并发型的 GC 是与应用程序一起运行的,也就是说,在 GC 运行的同时应用程序会持续地分配新对象。如果应用程序产生垃圾的速度超过了 GC 的回收速度,我们就有麻烦了。Shenandoah 开发者团队对这个回收器的故障模式也是直言不讳,并在文档中详细地描述了它们。

当垃圾回收速度赶不上垃圾生成速度时,Shenandoah 首先会尝试步调调整(pacing),即让分配对象的线程稍作停顿,降低垃圾生成的速度。这个与 STW 停顿有点像,但其实也不太像,因为它其实只影响个别线程,而不是整个应用程序。因为步调调整不作为 GC 停顿处理,所以监控工具很难看到它们,只能从 GC 日志里查找是否发生过步调调整。

如果这样还不行,Shenandoah 会进入退化模式,也就是进行老式的 STW GC,不一样的地方在于已经并发执行的 GC 工作不会重复执行。换句话说,如果 Shenandoah 能够及时进行并发回收,即使进入退化模式,停顿也较短,因为不需要在 STW 阶段完成所有的工作。与步调调整不一样的是,退化模式 GC 将被视为正常的停顿,因此监控工具可以看得到。如果你发现 Shenandoah 进入退化模式,说明创建对象的速度太快了。

最后,如果 Shenandoah 在退化模式下无法释放足够的内存,仍然会发生 STW GC。Shenandoah 的 FullGC 是并行的,所以至少会比单线程的 STW GC 快,但停顿仍然会比较长。幸运的是,我们在我们的工作负载中还没有碰到这样的 FullGC。

Shenandoah 调优

使用 Shenandoah 的默认配置就可以应对大部分场景,所以大部分情况下你不需要去修改配置。不过,其中有一个最重要的参数 -Xmx,只要通过这个参数指定足够大的堆内存,剩下的事情就交给 Shenandoah 了。不过,随着你对它有了进一步的了解,可以对它做出适当的调整,让它在各种特定的工作负载下运行得更好。

Shenandoah 的一个主要调节选项是 heuristic 类型,它会根据这个参数决定什么触发 GC。这个参数的默认值是 adaptive,有就是根据程序启动前几分钟对象的分配速速来推断 GC 的阈值。你也可以把它改成 static,并指定在剩余多少可用内存时触发 GC。如果你注重延迟而不是吞吐量,也可以把它设置为 compact,这样就不会发生步调调整或进入退化模式。我们最终选择了 compact,CPU 使用率不会再像之前那么高了。

6一些发现

大量的弱引用(软引用、虚幻引用、finalizer)会增加 Shenandoah 的停顿时间,因为这些引用需要在 STW 期间处理。即使应用程序中没有直接使用弱引用,但一些依赖的库或框架可能会用到。我们在项目中使用了内存泄漏检测机制,而这个机制又使用了 finalizer,所以,当我们在生产环境中禁用了内存泄漏检测机制,停顿时间就得到了大幅的改善。

一般来说,Java 垃圾回收器与同步块是不一样的,因为同步块会导致监视器膨胀并增加根对象集合。我们之前犯了一个错误,为了节省 50 字节的对象空间,使用同步类方法替代了 ReentrantLock。经过这个“优化”之后,Shenandoah 的停顿反而增加了,所以我们又回退到使用 ReentrantLock。

这个发现让我们感到很吃惊。不知道是什么原因,在运行了 7 天之后,Shenandoah 的性能逐渐下降,停顿时间也逐渐变长。经过一番调试,我们发现了一个由反射调用引起的类加载器泄漏。很显然,JVM 会在运行时通过反射的方式生成类,而这些类不会被卸载。我们目前通过设置 -Dsun.reflect.inflationThreshold=2147483647 来临时规避这个问题。

确保 Shenandoah 拥有足够的线程!当然,这个是由 Shenandoah 自行决定的,它会根据机器的 CPU 核数来决定使用多少个线程。不过,如果你刚好使用的是 Amazon ECS,并且运行在 JVM 9+ 上,而你又忘记为容器设置 CPU 共享,那么 Java 只会看到一个 CPU 核数!这个时候 Shenandoah 只使用一个线程来回收垃圾,那么整个应用程序的运行速度可想而知了。

7结论

我希望读者们在读完这篇文章后可以看看 Shenandoah 是不是可以解决你们的一些问题。如果可以,请分享你们的经验,这样就会有更多的人知道这个新垃圾回收器。
来自51CTO博客作者mb5fd86ddc9c8d5的原创作品

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值