3年前 (2021-08-16)  jvm |   抢沙发  1494 
文章评分 0 次,平均分 0.0

Shenandoah是低暂停时间垃圾收集器,它通过与运行的Java程序同时执行更多垃圾收集工作来减少GC暂停时间。Shenandoah同时执行大部分GC工作,包括并发压缩,这意味着它的暂停时间不再与堆的大小成正比。垃圾收集200 GB堆或2 GB堆应该具有类似的低暂停行为。

Shenandoah的可用性因供应商和JDK版本而异。OpenJDK 12+版本默认情况下通常包括Shenandoah。OpenJDK 11需要在构建时选择加入。

已知供应商状态为:

Red Hat

  • Fedora 24+OpenJDK 8+版本包括Shenandoah
  • RHEL 7.4+附带OpenJDK 8+,其中包括Shenandoah作为技术预览版
  • Red Hat OpenJDK 8u Windows版本包括Shenandoah

Amazon

  • 从OpenJDK 11.0.9开始,在Amazon Corretto中发布Shenandoah

Oracle

  • 不在任何版本中提供Shenandoah,包括OpenJDK版本和专有版本

Azul

  • 从OpenJDK 11.0.9开始,在Azul Zulu安装Shenandoah

SAP

  • (状态未知)
  • 采用OpenJDK
  • 从OpenJDK 11.0.9开始,以默认二进制文件发布Shenandoah

Linux发行版

  • Debian从OpenJDK 11.0.9开始发布Shenandoah
  • 用于IcedTea的Gentoo ebuild具有Shenandoah使用标志
  • 基于RHEL/Fedora的发行版或使用其软件包的其他发行版也可能启用了Shenandoah。值得注意的是,CentOS、Oracle Linux和Amazon Linux都是众所周知的产品。

Shenandoah是区域化收集器,它将堆作为区域region集合进行维护。

常规Shenandoah GC循环如下所示:

JDK新垃圾收集器-Shenandoah GC

性能指南和诊断

堆大小:与几乎所有其他GC的性能一样,Shenandoah性能取决于堆大小。当有足够的堆空间来容纳并发阶段运行时的分配时,它的性能应该更好(请参阅下面的“故障模式”部分)。并发阶段的时间与实时数据集大小(LDS)相关——实时数据占用的空间。因此,合理的堆大小取决于LDS和工作负载中的分配压力:对于给定的分配速率,较大的LDS和es需要成比例的较大堆大小;对于给定的LDS,更大的分配速率需要更大的堆大小。对于一些具有极小实时数据集和中等分配压力的工作负载,1…2 GB堆表现良好。我们经常在各种工作负载上测试4…128 GB堆,LDS大小高达80%。不要羞于尝试不同的堆大小,看看什么适合您的工作负载。

暂停:Shenandoah的暂停行为主要由根集操作控制:扫描和更新根。根集包括:局部变量、嵌入在生成代码中的引用、内部字符串、来自类加载器的引用(例如静态最终引用)、JNI引用、JVMTI引用。拥有更大的根集通常意味着使用Shenandoah的暂停时间更长,除非具体的JDK版本能够同时完成部分工作,并且Shenandoah能够使用它。二阶效应是:a)弱引用处理(发生在最后标记暂停时),但仅适用于需要处理的引用;和b)类卸载和其他JDK清理(也发生在最终标记暂停中)。这些二阶效应可以通过配置控制处理频率的附加选项(包括完全禁用)和/或修改应用程序以更好地发挥作用来缓解。

吞吐量:因为Shenandoah是并发GC,所以它在收集周期中使用屏障来保持不变量。这些障碍可能导致可测量的吞吐量损失。请参阅下面的“诊断”部分,了解如何剖析那里发生的情况。一些用户报告说,通过自然地将并发GC工作卸载到备用或空闲内核,可以弥补由于屏障而导致的吞吐量损失;换句话说,在某些情况下,它以更高的应用程序+JVM利用率换取更高的应用程序吞吐量。

在大多数情况下,暂停时间在0..10ms以内,吞吐量损失在0..15%以内。实际性能数字在很大程度上取决于实际应用程序、负载配置文件等。对于没有很多根、弱引用和/或类搅动的应用程序,暂停可能在亚毫秒范围内。如果应用程序没有对堆进行太多的变异,或者当前编译器对其进行了很好的优化,那么屏障开销可能接近于零。

基本配置

基本配置和命令行选项:

  • -Xlog:gc(从jdk9开始)或-verbose:gc(直到jdk8)将打印各个gc计时。
  • -Xlog:gc+ergo(从JDK 9开始)或-XX:+PrintGCDetails(直到JDK 8)或将打印启发式决策,这可能会揭示异常值(如果有的话)。
  • -Xlog:gc+stats(从JDK 9开始)或-verbose:gc(直到JDK 8)将在运行结束时打印Shenandoah内部计时的汇总表。

在启用日志记录的情况下运行几乎总是一个好主意。这个汇总表传达了有关GC性能的重要信息,我们几乎不可避免地会在性能缺陷报告中要求提供一个。启发式日志有助于找出GC异常值。

其他推荐的JVM选项包括:

  • -XX:+AlwaysPreTouch:将堆页提交到内存中有助于减少延迟中断
  • -Xms-Xmx:使用-Xms=-Xmx使堆不可调整大小可以减少堆管理带来的问题。再加上AlwaysPreTouch-Xms=-Xmx将在启动时提交所有内存,从而避免在最终使用内存时出现问题-Xms还定义了内存未提交的下限,因此使用-Xms=-Xmx时,所有内存都将保持提交状态。这就是说,如果您想要配置Shenandoah以降低占用空间,那么建议设置lower-Xms。您需要决定将其设置为多低,以平衡提交/未提交开销与内存占用。在许多情况下,将-Xms设置为任意低就可以了。
  • 使用large pages可以极大地提高大型堆的性能。有两种选择加入的方式-XX:+UseLargePages将启用hugetlbfs(Linux)或Windows(具有适当权限)支持-XX:+UseTransparentHuggePages将以透明方式启用它。对于透明的巨大页面,建议将/sys/kernel/mm/transparent_hugepage/enabled/sys/kernel/mm/transparent_hugepage/defrag设置为“madvise”。当使用AlwaysPreTouch运行时,它还将在启动时预先支付碎片整理成本。
  • -XX:+UseNUMA:虽然Shenandoah还没有明确支持NUMA,但启用它以在多套接字主机上启用NUMA交错是一个好主意。再加上AlwaysPreTouch,它提供了比默认的开箱即用配置更好的性能
  • -XX:-UseBiasedLocking:在无竞争(有偏见)锁定吞吐量和safepoints JVM根据需要启用和禁用它们之间存在折衷。对于面向延迟的工作负载,关闭偏置锁定是有意义的。
  • -XX:+DisableExplicitGC:从用户代码调用System.gc()会强制Shenandoah执行额外的gc循环;禁用此功能可能有益于防止滥用System.gc()的代码。它通常不会造成伤害,因为默认情况下会启用-XX:+ExplicitGCInvokesConcurrent,这意味着将调用并发GC循环,而不是STW完整GC。

模式

模式定义了Shenandoah运行的主要方式。这定义了Shenandoah使用的屏障(如有),并定义了主要性能特征。可以使用-XX:ShenandoahGCMode=<name>选择模式。可用的模式有:

  • normal/satb(产品,默认)。此模式在开始时使用快照(SATB)标记运行并发GC。这种标记模式类似于G1所做的:截取写操作并通过“先前”对象进行标记。
  • iu(实验性)。此模式运行带有增量更新(IU)标记的并发GC。此标记模式与SATB模式类似:通过“新”对象截取写入和标记。这可能会降低标记的保守性,尤其是在访问弱引用时。
  • passive(诊断)。此模式运行停止全球地面军事系统。此模式用于功能测试,但有时它用于将性能异常与GC屏障平分,或计算应用程序中的实际实时数据大小。

失效模式

像Shenandoah这样的并发GC隐式地依赖于比应用程序分配更快的收集速度。如果分配压力很大,并且在GC运行时没有足够的空间来吸收分配,则最终会发生分配失败。Shenandoah有一个优雅的退化阶梯,有助于在这种情况下生存。梯子由以下部分组成:

1. 调整(-XX:+ShenandoahPacing,默认情况下启用)。当GC运行时,它知道需要完成多少GC工作,以及应用程序有多少可用空间。Pacer会在GC进度不够快时尝试暂停分配线程。在正常情况下,GC收集的速度比应用程序分配的快,pacer自然不会停止。请注意,起搏引入了在常规分析工具中不可见的本地每线程延迟。这就是为什么暂停不是无限期的,它们以-XX:ShenandoahPacingMaxDelay=#ms为界。在最大延迟到期后,分配无论如何都会发生。大多数情况下,轻微的分配峰值被起搏器吸收。当分配压力很大时,pacer将无法应对,降级将进入下一步。

通常诱发的潜伏期:<10毫秒

2. Degenerated GC(-XX:+ShenandoahDegeneratedGC,默认启用)。如果应用程序运行到分配失败,那么Shenandoah将跳入停止世界暂停,停止整个应用程序,并在暂停下继续循环。退化GC在“停止世界”下继续进行“并发”循环。在许多情况下,分配失败发生在大量GC工作已经完成之后,并且需要完成一小部分GC工作。这就是STW暂停通常不大的原因。它将在GC日志中报告为GC pause,即所有常用的监视和心跳线程:实际上,导致STW暂停的原因之一是使并发模式故障清晰可见。如果GC循环开始得太晚,或者发生了非常显著的分配峰值,则可能会发生退化GC。退化周期可能比并发周期快,因为它不与应用程序争夺资源,并且它使用-XX:ParallelGCThreads,而不是-XX:congcthreads来调整线程池大小。

通常诱发的潜伏期:<100 ms,但可能更久,具体取决于退化点

3. Full GC。如果没有任何帮助,例如,当退化的GC没有释放足够的内存时,将发生完整的GC循环,并将堆压缩到最大。某些场景,如异常碎片化的堆加上实现性能错误和忽略,将仅由完整的GC修复。如果至少有一些内存可用,这个最后的GC保证应用程序不会因OOM而失败。

通常引起的延迟:>100毫秒,但可以更长,尤其是在占用非常多的堆上

性能分析

了解GC暂停可能不是常规应用程序中响应时间的唯一重要因素,这一点很重要。大GC暂停很可能会导致响应时间问题,但缺少长GC暂停并不总是意味着有足够的响应时间。排队延迟、网络延迟、其他服务延迟、操作系统调度程序抖动等可能是造成成本增加的原因。建议运行带有响应时间测量的Shenandoah,以全面了解系统中正在发生的事情,然后可以使用它与GC暂停时间统计数据关联。

例如,这是一个jHiccup关于以下工作负载之一的示例报告:

JDK新垃圾收集器-Shenandoah GC

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/2232.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册