在实时系统中控制Java垃圾收集

5
我们正在使用Java运行一个RT系统。它经常使用相对较大的堆(100+GB),并处理来自消息队列的请求。每个请求必须快速处理(<100ms)以满足SLA。
我们遇到了严重的GC相关问题,因为通常情况下GC会在请求期间引起停顿(200+ms),导致失败。
我们的一位开发人员具有合理的GC知识,花费了相当长的时间来调整GC参数并尝试不同的GC。几天后,他提出了一些参数设置,我们戏称其为“基于遗传算法演化而来”。它降低了GC暂停时间,但仍远未达到满足SLA要求的水平。
我寻找的解决方案是从GC中保护某些关键代码部分,在请求完成后,让GC尽可能多地工作,然后再处理下一个请求。偶尔在请求之外暂停是可以接受的,因为我们有几个工作者,进行垃圾回收的工作者只需暂停一段时间即可。
我有一些想法,它们看起来很愚蠢、丑陋,并且很可能不起作用,但希望它们能说明问题:
  • 在接收线程中偶尔调用Thread.sleep(),希望GC在此期间能够起到一些作用,
  • 在请求之间调用System.gc()Runtime.gc(),再次无望地祈求它能有所帮助,
  • 使用类似https://dev59.com/FXM_5IYBdhLWcg3wNgKY#6915221这样的hacky模式来混淆代码。

最后一个重要的注意事项是,我们是一个低预算的初创公司,商业解决方案如Zing®对我们来说不是一个选项,我们正在寻找非商业解决方案。

有什么想法吗?我们会完全重写我们的代码到C++(我们一开始并不知道GC可能是问题而不是解决方案),但是代码库已经太大了。


2
无论如何,在长时间运行的进程中,实际上只有两种常规方法可以解决GC问题:(1)减少产生的垃圾量,和(2)使垃圾更快地被收集。如果完整的GC代价高昂但不频繁,则一个替代方案可能是大大减少堆大小。这将需要更频繁的GC,但每个GC应该会更快,因为要收集的垃圾不能太多。此外,尽量避免长期存在的对象,因为对于分代GC来说,它们的成本更高,除非它们在整个进程的生命周期内都被保留。 - John Bollinger
2
此外,要注意临时对象。Java程序员很容易通过创建和丢弃大量对象来过度依赖GC,他们甚至可能没有意识到自己在这样做。例如,字符串连接和自动装箱可能会对此产生影响。原始类型没有GC成本,作为一个经验法则,较低级别的API产生的垃圾较少。 - John Bollinger
1
关于保护关键代码免受GC的影响,Java没有直接适用于该目标的API。如果有的话,使用它会引入一个风险,即关键代码可能会因为OutOfMemoryError而失败,而在其他情况下只会被GC延迟。这将会造成更大的混乱。 - John Bollinger
@JohnBollinger 感谢您的笔记,看起来我们确实产生了太多的垃圾,例如在每个请求期间在8个线程中单独构建大型HashMap<String,Double>对象,意味着有很多Double包装器而不是基元类型,所有这些最终都被丢弃。我们会认为提供足够的RAM以防止OutOfMemoryError是我们的责任,但我理解您的观点。考虑到您所说的,您认为创建一个共享的HashMap<String,Double>池进行回收是否有帮助,或者我们的大部分问题来自包装器? - Tregoreg
1
@Tregoreg,大型HashMap图中几乎所有对象都与条目相关联;无论您是否重用地图,它们都将变为垃圾。如果这些是问题的重要因素,则可能想出一种用double[]替换它们的方法会有所帮助,前提是这样做不需要在其他地方创建与节省的地图数量一样多的额外对象。 - John Bollinger
显示剩余4条评论
1个回答

3
有什么想法吗?尝试使用不同的JVM? Azul声称能够处理这种情况。Redhat和Oracle分别向openjdk贡献了shenandoah和zgc,并具有类似的目标,因此如果您不想要商业解决方案,则可以尝试实验性构建。还有其他专注于实时应用程序的JVM,但据我所知,它们关注更小系统上更难的实时要求,而您的要求似乎更像是软实时要求。另一件事是通过使用适用的预分配对象或更紧凑的数据表示(分析应用程序!)来显着减少对象分配。在保持新生代大小不变的同时减少分配压力意味着每个集合的死亡率增加,这应该会加快年轻人的集合速度。选择最大化内存带宽的硬件也可能有所帮助。
在请求之间调用System.gc()或Runtime.gc(),再次无望地祈求其有所帮助,这种方法结合使用-XX:+ ExplicitGCInvokesConcurrent 可能有效,否则将触发一个单线程STW集合与CMS或G1(我假设你正在使用其中之一)。但这种方法看起来脆弱,需要大量调整和监视。

我尝试了几次 System.gc() + -XX:+ExplicitGCInvokesConcurrent,但它确实不起作用,主要是因为 System.gc() 运行了几分钟。运行完整的 GC 似乎不是一个选项。是的,我们尝试了 CMS 和 G1。如果没有其他办法,将不得不尝试替代的 JVM 和分析+重构。然而,越想越多,我越相信在我的情况下告诉 JVM 何时进行收集正是所需的。原则上,调整 GC 参数不能解决我的问题,除非给 JVM 提供额外的信息。 - Tregoreg
好的,你说暂停时间为200毫秒,而你的SLA是<100毫秒。因此,如果你能挤出2-3倍的因素,似乎是可以达到的。 - the8472

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