对象不断晋升到老年代

3
我正在使用一个提供REST API的应用程序。它只处理GET请求,即使是最重的请求也通常需要100毫秒。
最近我们开始遇到一个问题,时不时地堆会被填满,全GC需要很长时间,这真的影响到了我们的客户。
一些额外的发现:
- 我们启用了JVM标志-XX:+PrintTenuringDistribution,根据此标志输出,新的tenuring阈值在应用程序启动后几分钟内变为1,即使没有显着负载。 - 当应用程序消耗了几乎所有允许的内存时,我们进行了内存转储,并根据MAT分析器的统计数据,几乎所有在转储中的对象都是不可达的(因此,我无法理解为什么它们会被提升到老年代)。 - Survivor空间几乎没有使用。看起来新对象直接从Eden空间晋升到Old Gen。 - 每10-20秒钟进行一次Minor GC。 - 增加Eden / Survivor空间没有帮助解决问题(tenuring阈值仍为1)。
因此,请问:
- 为什么tenuring阈值会这么快变为1?[已解决] - 还可以采取哪些附加步骤以避免将新对象提升到旧代?
以下是应用程序的一些参数:
- 堆大小 - 5GB - 每秒并发请求数量 - 高达50个 - 每个请求最多消耗1-2MB(有些请求会消耗12 MB的堆内存) - 应用程序同时使用Parallel GC进行Old和young geerations。 - JDK 1.7
请让我知道是否需要其他信息。
-XX:MaxPermSize=512m -Xmx7g -Xmn4g -verbose:gc -Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintTenuringDistribution

应用程序日志:

Desired survivor size 520617984 bytes, new threshold 15 (max 15)
 [PSYoungGen: 3235245K->231763K(3665920K)] 3942774K->939308K(4760064K), 0.0905430 secs] [Times: user=0.31 sys=0.00, real=0.09 secs] 
603.561: [GC
Desired survivor size 521142272 bytes, new threshold 15 (max 15)
 [PSYoungGen: 3369299K->330881K(3684864K)] 4076844K->1038442K(4779008K), 0.1343473 secs] [Times: user=0.51 sys=0.00, real=0.13 secs] 
606.347: [GC
Desired survivor size 506462208 bytes, new threshold 15 (max 15)
 [PSYoungGen: 3507329K->215655K(3685376K)] 4214890K->923233K(4779520K), 0.0925060 secs] [Times: user=0.36 sys=0.00, real=0.09 secs] 
609.084: [GC
Desired survivor size 492306432 bytes, new threshold 15 (max 15)
 [PSYoungGen: 3392103K->213344K(3713536K)] 4099681K->920945K(4807680K), 0.0802729 secs] [Times: user=0.30 sys=0.00, real=0.08 secs] 

重要细节:

即使我只使用1个线程(每个请求消耗约10 MB),启动性能测试后,老年代仍会增长: Old Gen with 1 thread perf test

如果我启动GC,它可以成功清理内存

如果我进行堆转储,则Mat Analyzer(或Yourkit)再次显示80%的对象无法访问


对象相对于幸存者空间有多大?如果一个对象太大而无法适应可用的伊甸园空间,那么它将立即被提升。这就很糟糕了,因为即使所有对它的引用都已经消失,它也不会在老年代空间进行垃圾回收。例如,创建包含Json或Xml的大字符串对象的其余Apis可能会遇到此问题。 - Chris K
1
一个相关的陷阱也可能是如果经常使用变量数据调用string.intern。比如一个xml字符串。 - Chris K
1
以下文章可能会对您有所帮助。http://www.javaaddicts.net/blogs/-/blogs/understanding-premature-promotion-and-how-to-avoid-it - Chris K
@the8472,我已经在问题中添加了JVM参数。请查看更新。 - evgeniy44
@evgeniy44为什么一个线程不能生成足够的内容来溢出survivor空间?请检查GC日志,确认所需的survivor大小是否已满足?您还确认了分配是否不会转到String.intern或Unsafe或直接字节缓冲区,并且对象大小都在预老年代阈值/ TLAB大小以下吗?Java被优化用于处理许多小对象(以字节为单位),大对象可能会引起问题。一个大对象是指任何无法适合TLAB缓冲区的对象...这并不是非常大,我认为默认情况下它是以kb为单位衡量的。 - Chris K
显示剩余19条评论
1个回答

1
如果您的tenuring阈值快速下降到1,那么似乎有其他参数导致了这种情况。除了日志选项和最大堆之外,我建议删除所有其他调整参数,并在确定它们确实有帮助时逐个添加每个参数。您设置的越多,出现奇怪行为的可能性就越大。您可以尝试强制执行tenuring阈值,但我怀疑这只能在最好的情况下隐藏您的问题。
为了增加幸存者大小以避免完全触发幸存者的全GC,幸存者大小或仅一个代使用整个年轻代空间。
我建议将Young generation(年轻代)变得更大。你需要至少有一个Eden survivor space,所以你的年轻代应该至少是1 GB,理想情况下是几倍于此。我建议尝试4 GB年轻代或2 Gb,具体取决于您拥有多少空间。例如:
-Xmn2g -Xmx5g

或者

-Xmn4g -Xmx7g

如果您有充足的内存,例如64 GB或更多,我建议将其调整得更大。
 -Xmn24g -Xmx32g

如果看起来没有帮助,可以减小年轻空间。

请查看问题更新中的参数。没有其他可能影响该过程的参数。 - evgeniy44
我已经添加到我的答案中。你有多少内存?你能在至少有32GB可用内存的机器上尝试吗? - Peter Lawrey
很遗憾,我只能提供5GB的堆大小。另外,请看一下我的最新发现(使用单线程进行性能测试)。对于单线程来说,2.5GB的年轻代空间应该足够了。 - evgeniy44
1
我使用 -Xmn4g -Xmx7g 运行应用程序,阈值变为15(有时会更改,但很快又回到15)。但是我仍然看到新对象被提升到老年代。 - evgeniy44
2
@evgeniy44 这意味着您有一些中等寿命的对象,它们的寿命足够长,可以经受住15次小型收集。您可以进一步增加年轻代空间,但在复杂的应用程序中,总会有一些对象的生命周期稍微长一些。增加年轻代空间可以增加收集之间的时间,使其更不可能但并非不可能经受住15次收集。 - Peter Lawrey

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