Java垃圾回收器时间限制

5

情况

我正在使用Java开发一个客户端参加比赛,每当我收到请求时,我有两秒钟的时间来做出响应。从响应到下一个请求的时间未知。

有时,找到正确的响应需要近2秒钟,有时只需要几毫秒。问题在于,当长时间计算(还分配了大量对象)中的垃圾回收在两秒钟结束前发生时,响应就会被发送太晚,我就会被取消资格。

使用verbose gc输出,我确定gc通常需要约0.6秒,尽管我试图将其限制在更低的时间。我还尝试在短计算上调用System.gc()(因为我确定有约1.8秒不需要做任何事情),但它需要1-3秒,这也不安全。

我的程序中几乎没有长期存在的对象,大多数对象生存周期都不到一秒。

规格

我知道该程序将始终运行在同一台机器上,并可用以下资源:

我的当前jvm参数:

java -Dfile.encoding=UTF-8 \
  -XX:MaxGCPauseMillis=200 \
  -XX:GCPauseIntervalMillis=2050 \
  -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled \
  -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 \
  -XX:+ScavengeBeforeFullGC -XX:+CMSScavengeBeforeRemark \
  -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps

想法

  • 我是否可以告诉垃圾回收器现在应该回收一些垃圾,但只要大约1.5秒钟?
  • 是否有一个System.gc()等效的方法只使用于年轻对象,并且不检查老年代?
  • 是否可以优化jvm参数以取得更好的结果?

据我所知,您无法真正限制垃圾回收。您只能尝试在计算期间保持对所有变量的引用,这样GC肯定不会运行(因为所有内容仍然被引用,因此无法释放)。然后,在计算后取消对所有内容的引用,以便GC可以运行(并且可以使用System.gc()来希望您的垃圾回收得到提示)。 - Ben
不要试图优化Java垃圾收集(这就像是说你想给哥斯拉化妆),你可能想尝试关注更大的问题。为什么每当您收到请求时,Java都需要执行GC?虽然GC确实经常发生,但如果您找到了一个模式,那么就是您的程序导致它发生。您可能需要好好看看背后的原因。 - SoroushA
2
你尝试过使用 G1 垃圾回收吗?它具有更频繁的微循环,几乎不会被注意到。System.gc() 仅仅是一个建议,在现代的垃圾回收器中没有明显的效果。请用以下参数替换所有 GC 参数:--XX:+UseG1GC。 - AlexC
学习C++的时间到了 - AdamSkywalker
你说“在现代收集器中没有任何明显的作用”。那是不正确的。在大多数现代JVM中,默认情况下会触发GC。这非常明显。当然,如果GC已经在运行,再次触发它就没有效果。但这也有明显的原因... - Stephen C
显示剩余5条评论
2个回答

2

经过多次试验,我找到了需要的标志并想要分享这些知识:

  • -XX:+UseConcMarkSweepGC - 在比较了我的使用情况下 G1 和 CMS 的日志结果后,我确定 CMS 暂停时间更短,此外它仍然支持单线程收集
  • -XX:+ExplicitGCInvokesConcurrent - 调用 System.gc() 时不会触发完整的 GC,而是标准的 GC。
  • -XX:NewRatio=1 - 老年代与新生代的大小比例,由于我只有很少的长期存活对象,因此这是最低的值
  • -mx800m -ms800m - 减少和固定内存大小,从而使集合更频繁且所需时间更少。以响应速度为代价来换取吞吐量。
  • -XX:-UseParNewGC - 禁用并行收集年轻代。这是一个关键点,可以将 GC 停顿时间从 0.2-0.5 秒降至 0.02-0.2 秒,因为我只有一个可用的核心。(在新版本的 Java 中,将此与 CMS 结合使用已被弃用并可能被删除)

使用这些参数,我可以将 GC 暂停时间从 0.4 秒至 3 秒不等降至始终少于 0.2 秒。为了完整起见,这些标志对调试最有用:

-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution

请不要忘记,这些是针对非常特定的要求进行优化的:只有一个可用核心,大量年轻垃圾,没有旧的集合,非常低的停止-全球暂停。
这是我推荐进一步阅读的小抄表:http://blog.ragozin.info/2016/10/hotspot-jvm-garbage-collection-options.html

2
以下是您可以尝试的减少暂停时间的方法:
  • 尝试使用G1收集器,而不是CMS收集器
  • 使用更现实的GC目标。当前目标允许每2秒有200毫秒的停顿式GC时间。增加MaxGCPauseMillis和/或减少GCPauseIntervalMillis。
  • 增加另一个核心,以便JVM能够与您的应用程序并行进行GC。
  • 降低CMSInitiatingOccupancyFraction,以使GC在堆填充较少时触发后台GC线程。
  • 减少应用程序生成垃圾的速率。
  • 调整应用算法,使单个核心有更多的空闲时间用于后台GC。

“增加另一个核心”,竞赛系统不在我的控制之下。“减少应用程序生成垃圾的速率。”我必须不断克隆BFS的对象,所以这也不是一个选项。我认为G1针对更高性能的环境进行了优化,但目前看起来很好。我唯一想知道的是,为什么它完全没有记录任何活动,尽管我在其中留下了-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps - xeruf
1
“我必须不断地为BFS克隆对象,所以这也不是一个选项。” - 广度优先搜索不应该需要任何克隆。现在,你的“不断克隆”可能是一种方便的方法来做某些事情,但如果你重新设计/编写你的算法,你很可能可以避免它。虽然这可能不是一个有吸引力的选择,但这是一个选项。(这取决于你想赢得比赛的程度 :-)) - Stephen C
另一个问题:两个现代GC似乎总是与多个工作线程一起工作,考虑到我只有一个核心,这听起来并不高效。因此,我希望我的GC是“并发的”,但不是“并行的”,这可能吗? - xeruf
基本上不会。JVM是多线程的,GC使用线程来运行GC工作线程。如果您没有多个核心,则线程将被操作系统分时。 - Stephen C

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