调整垃圾回收以实现低延迟

19

我正在寻找关于在低延迟环境中如何最好地确定年轻代(相对于老年代)大小的论据。

我的测试结果表明,当年轻代相当大时(例如-XX:NewRatio <3),延迟最小,但是我不能将此与更大的年轻代应该需要更长时间进行垃圾收集的直觉调和。

应用程序在Linux 64位,jdk 6上运行。

内存使用量约为50兆字节的长期存在的对象在启动时被加载(=数据缓存),从那里开始只有(许多)非常短暂的对象被创建(平均寿命<1毫秒)。

有些垃圾回收周期需要超过10毫秒才能运行...与应用程序的延迟相比,这看起来确实不成比例,而应用程序的延迟最多只有几毫秒。


如果年轻一代很多,那么半长寿命的物品会消失在廉价的年轻一代收藏中,使你拥有更少的昂贵老一代收藏,对吗? - gustafc
1
@elec:我无法在这里帮助你……但是你的问题表明了Java及其“自动”(现在看起来它是自动化的,对吧;)内存管理的一个基本问题。我使用Java的时间越长(十年了),我就越希望自己能够管理内存(是的,我曾经使用过需要手动管理内存的语言,包括几十年前的二代语言[直接输入十六进制代码])。在Java世界中浪费了多少时间和精力去理解和“微调”那个不确定性的GC......本应该是一个不成问题的事情,却变成了一个让我想回到C++的问题。 - SyntaxT3rr0r
@WizardOfOdds - 我同意你的观点...尽管我很喜欢Java,但在需要非常低延迟的环境中使用它似乎不是正确的工具。 - Eleco
2
你需要描述应用程序的分配行为,并明确你所指的延迟是什么,以及在这种情况下什么是低延迟(1微秒、10微秒、100微秒?)。当你说“垃圾回收应该花费更多时间”时,听起来你真正指的是由于GC事件引起的暂停时间,而不是应用程序的延迟。YG暂停时间与伊甸园大小并不成比例增长。 - Matt
@Matt,请看新编辑的问题。 - Eleco
3个回答

14

针对产生大量短暂垃圾且没有长期存活对象的应用程序,可以采用一种有效的方法,即使用大型堆,将几乎所有资源配置为年轻代,并将 Eden 区和幸存者区(Survivor Space)中的任何内容都升级到老年代,同时保留那些在 YG 回收超过一次的对象。

例如 (假设你有一个32位JVM):

  • 3072M 堆大小(包括 Xms 和 Xmn)
  • 128M 老年代(即 Xmn 2944m)
  • MaxTenuringThreshold=1
  • SurvivorRatio=190 (即每个幸存者区的大小为YG大小的1/192)
  • TargetSurvivorRatio=90 (即尽可能填充那些幸存下来的对象区域)

确切的参数取值要根据你的工作集合稳态大小来决定,也就是说,在每次垃圾回收时有多少存活对象。这里的思路显然违背了正常的堆大小规则,但是你的应用程序也不是按照正常方式运行的。这个思路的核心是,应用程序主要生成非常短暂的垃圾数据和一些静态数据,因此要将 JVM 配置成这些静态数据快速进入老年代,并且要有足够大的 YG 区域,使它不会经常被回收,从而最小化暂停的频率。你需要反复调整参数以找出适合自己的堆大小,并平衡每次垃圾回收带来的暂停时间。例如,你可能会发现更短但更频繁的 YG 暂停是可以实现的。

你没有说你的应用程序运行多长时间,但这里的目标是在应用程序的生命周期内完全避免老年代垃圾回收。当然,这可能是不可能的,但值得努力。

然而,在您的情况下,不仅是收集算法很重要,还有内存分配的位置。NUMA收集器(仅与吞吐量收集器兼容,并使用UseNUMA开关激活)利用的观察是,对象通常只由创建它的线程使用,因此相应地分配内存。我不确定它在Linux上基于什么,但在Solaris上使用MPO(内存放置优化),这是GC专家博客中的一些详细信息

由于您正在使用64位JVM,请确保您也使用了CompressedOops。

考虑到对象分配速率(可能是某种科学库)和生命周期,您应该考虑对象重用。一个做到这一点的库的例子是javalution StackContext

最后值得注意的是,GC暂停不是唯一的STW暂停,您可以运行6u21早期版本构建版,其中包含一些修复PrintGCApplicationStoppedTime和PrintGCApplicationConcurrentTime开关的内容(有效地打印全局安全点的时间以及这些安全点之间的时间)。您可以使用tracesafepointstatistics标志来了解是什么导致需要安全点(也就是没有任何线程执行字节码)。


5
您是否已经启用了更相关的GC设置,比如选择并发低暂停收集器算法?
总的来说,年轻、终身和永久的各代需要根据应用程序的配置进行大小调整。如果您有许多短寿命对象,但年轻代太小,则许多对象将变为终身代,迫使更频繁地对整个终身代进行主要收集。同样,如果年轻代太大,则终身代必然较小,并可能迫使频繁对终身代进行主要收集。
实际上,在实践中,随着增加年轻代的大小,次要收集与主要收集的时间交替进行,并在某一点达到最优状态。
也许值得注意的是,在“大型”性能敏感的服务器应用程序中,通常需要缩小年轻代。这是因为这些应用程序应该已经针对内存分配热点进行了优化,所以它们会产生很少的短寿命对象。这反过来意味着年轻代占用了太多的堆空间。
因此,我认为首先应该进行这种优化,然后再考虑将NewRatio调高到8以上,并观察-verbose:gc给出的输出,看GC和Full GC时间如何交替进行,并找到最优状态。

FYI:我搜索了如何启用上述低暂停收集器,并找到了这个看似相关的信息: http://java.sun.com/docs/hotspot/gc5.0/gc_tuning_5.html#1.1.%20Types%20of%20Collectors|outline 请参见链接中锚点下面的第2项。它建议使用“-Xincgc”或“-XX:+UseConcMarkSweepGC”。 - Chris Dolan

1

在使用Java进行实时应用程序时,垃圾收集调优是必不可少的,但您还需要考虑其他方面(例如JIT编译器、定时器、线程、异步事件处理)。

由于似乎存在对实时Java的需求,Sun提供了Java实时系统规范,并提供了商业实现。您可以在这里找到更多信息。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接