一个简单用例的JVM GC设置

5

我正在努力获取适合我的JVM的正确设置。

这是使用情况: Tomcat正在处理请求(300req/s)。但它们非常快(键值查找),所以我没有任何性能问题。一切都很好,直到我需要每3小时刷新它正在提供的数据。您可以想象我有一个大HashMap,我只是在进行查找。在数据重新加载期间,我创建了一个临时HashMap,然后将其交换。我需要加载相当多的数据(每次在内存中约800MB)。

我遇到的问题是,在这些加载期间,Tomcat不时停止响应。 最初的问题是晋升失败和FullGC,但通过调整设置解决了这些问题。

正如您可能已经注意到的那样,我已经降低了CMS收集器启动时的值。我不再遇到任何晋升失败或类似的问题。年轻一代足够小,使得小集合快速完成。我增加了SurvivorRatio,因为所有请求对象都会很快消失,而其他对象(即要加载的数据)应自动晋升到老年代。

但是,在数据加载期间,我仍然看到Tomcat中的503错误。在gc.log中,我的小集合开始变慢。它们现在需要秒来比较毫秒。我尝试减慢加载过程以让GC喘口气,但似乎不起作用... 问题特别严重的时候是我达到老年代容量的时候。CMS启动,释放内存,然后分配变得非常缓慢。我不再在gc.log中看到任何错误。 我应该怎么做才能有所不同?我知道碎片化可能是一个问题,但我没有遇到晋升失败。机器是8核服务器。减少GCThread的数量有意义吗?为数据加载线程设置较低的线程优先级是否有意义?

有没有办法定期在后台启动CMS收集器?实际上可以立即回收要交换的数据。

我对任何建议都持开放态度!

以下是我的JVM设置。

-Xms14g
-Xmx14g
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
-XX:+AlwaysPreTouch 
-XX:MaxNewSize=256m
-XX:NewSize=256m 
-XX:MaxPermSize=128m 
-XX:PermSize=128m 
-XX:SurvivorRatio=24 
-XX:+UseCMSInitiatingOccupancyOnly 
-XX:CMSInitiatingOccupancyFraction=88 
-XX:+UseCompressedStrings 
-XX:+DisableExplicitGC 

JDK 1.6.33

Tomcat 6

gc.log片段:

第7行开始数据加载

第20行停止

http://safebin.net/9124


你能否发布一下你的GC日志片段(带有-XX:+PrintGCDetails)?在进行数据加载时发布一个片段以及在一切正常时发布一个片段会很好。 - K Erlandsson
2个回答

2
查看附加的日志并看到小GC时间的巨大增加,我认为您的计算机受到其他进程(而非JVM)的极重负载。
我的推理是,在进行小GC时,所有应用程序线程都会停止。因此,您的应用程序不应该影响小GC时间,因为您的新生代大小是恒定的。
但是,如果在此期间有许多来自其他进程的负载,则GC线程将争夺执行时间,并且您可能会看到这种行为。
您能否在数据加载运行时检查其他进程的CPU使用情况?
编辑:仔细查看日志后,我想到了另一个可能的解释。
似乎目标幸存者空间已满(每个“慢”GC ParNew都下降到完全相同的10048K)。这意味着对象直接晋升到旧代,这可能会减慢速度。我建议尝试增加新生代的大小并降低幸存者比率。甚至可以尝试在不设置新生代大小或幸存者比率的情况下运行,并查看JVM如何优化此过程(尽管请注意,JVM通常无法很好地针对此类突发情况进行优化)。

嗨,Kristoffer!感谢您的反馈。回答您的问题:
  1. JVM正在运行在专用服务器上,没有其他东西在运行。TOP显示大约15%的CPU利用率,在数据加载期间仅增加(达到40%左右)。
  2. 至于Survivor空间,实际上我认为直接将对象提升到老年代是一件好事。如果它们在第一次小型收集中幸存下来,我们知道它们将成为长寿对象,因此在s0和s1之间复制它们有什么意义呢?也许我误解了它,我会尝试更改设置。
- Kuba
如果您真的想将所有活动对象提升到老年代,您应该尝试设置-XX:MaxTenuringThreshold=0,这样可以完全绕过幸存者空间。像现在似乎有太小的幸存者空间仍然会导致复制整个幸存者空间,直到对象达到 tenuring 阈值以及提升未适合完整幸存者空间的对象。这可能是为什么您的小 GC 要花费如此长时间的原因。 - K Erlandsson
我进行了一些测试,看起来降低SurvivorRation就可以解决问题!太棒了!我将进行一些额外的测试,使用MaxTenuringThreshold和-XX:TargetSurvivorRatio来查看它们的影响。 那么为什么直接从Eden晋升比从Survivor区域晋升更糟糕呢? - Kuba

1

你的负载大约持续了90秒,每1秒左右被GC中断一次,但你有一个14G的堆,其稳定状态的占用(假设周围的日志行都是稳态)只有约5G,这意味着你浪费了很多内存。根据所呈现的数据,我认为前面的答案看起来是正确的,即你的幸存者空间太小了。如果它在其余时间内除了查找不做其他操作,那么完全合理的策略可能是像下面这样:

  • tenuring阈值=0(或1)
  • eden大小>工作集的2倍,因此可能是1.5-2G(即允许当前活动数据和工作副本完全驻留在eden中)
  • tenured = 剩下的任何东西

这里的重点是尝试在加载阶段完全避免young collection。然而tenured阈值为0意味着以前的版本很可能在tenured中,你最终会看到一个可能很长的收集过程来清理它。另一个选择可能是反过来,使tenured足够大以适应2-3个数据版本,而将其余部分置于eden中,以尝试最小化young collection的频率,并帮助尽可能快地收集tenured。

最好的选择取决于应用程序在其余时间内的操作。

顺便说一下,对于大堆来说,CMS触发器似乎相当高,如果您只在88%开始收集,那么它是否有时间在强制进行fullgc之前完成工作?我想如果您大部分时间实际上很少分配,则可能非常安全。


我会尝试两个建议。(@Kristoffer @Matt) 谢谢。 至于内存浪费,这只是一个简化的例子。实际上,我可能有几个需要定期重新加载的大HashMaps,因此堆占用率将更接近10G。(在这种情况下增加伊甸园空间到4-5G可能不可行)我还记得尝试过使用大年轻代,从我记忆中来看,次要收集时间会增加。将所有分配的对象复制到老年代需要一些时间。这应该是一个高度响应的系统,我可以牺牲吞吐量以获得低延迟。 - Kuba

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