偶尔经历长时间的垃圾收集延迟,为什么?

我很难处理Java垃圾收集问题,并解释日志。

我的应用程序要求GC不需要超过2秒,理想情况下小于100毫秒。

根据以前的一些建议,我正在尝试以下命令行选项:

java -XX:MaxGCPauseMillis=100 -XX:NewRatio=9 -XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -server -Xmx9g -Xms9g 

该应用程序具有大量长期存储的对象,这些对象保存在ConcurrentLinkedHashMap中。 我偶尔会看到长时间停顿,最糟糕的情况是长达10秒(这是下面的GC日志中的第二个)!

以下是我得到的一些输出:

 16938.968: [GC 16938.968: [ParNew: 153343K->17022K(153344K), 7.8608580 secs] 6184328K->6122510K(9420160K) icms_dc=7 , 7.8614100 secs] [Times: user=0.63 sys=0.01, real=7.86 secs] 16947.087: [GC 16947.087: [ParNew: 153342K->17022K(153344K), 7.2604030 secs] 6258830K->6198642K(9420160K) icms_dc=7 , 7.2609780 secs] [Times: user=0.44 sys=0.00, real=7.27 secs] 16954.614: [GC 16954.614: [ParNew: 153342K->17024K(153344K), 8.4307620 secs] 6334962K->6274625K(9420160K) icms_dc=7 , 8.4313150 secs] [Times: user=0.62 sys=0.01, real=8.43 secs] 16963.310: [GC 16963.310: [ParNew: 153344K->17023K(153344K), 6.2588760 secs] 6410945K->6350748K(9420160K) icms_dc=7 , 6.2594290 secs] [Times: user=0.48 sys=0.01, real=6.25 secs] 16969.834: [GC 16969.834: [ParNew: 153343K->17022K(153344K), 6.0274280 secs] 6487068K->6425868K(9420160K) icms_dc=7 , 6.0279830 secs] [Times: user=0.50 sys=0.01, real=6.03 secs] 16976.122: [GC 16976.123: [ParNew: 153342K->17022K(153344K), 11.7774620 secs] 6562188K->6503030K(9420160K) icms_dc=7 , 11.7780180 secs] [Times: user=0.43 sys=0.04, real=11.78 secs] 16988.164: [GC 16988.164: [ParNew: 153342K->17024K(153344K), 10.9477920 secs] 6639350K->6579928K(9420160K) icms_dc=7 , 10.9483440 secs] [Times: user=0.37 sys=0.02, real=10.95 secs] 16999.371: [GC 16999.372: [ParNew: 153344K->17023K(153344K), 9.8828360 secs] 6716248K->6655886K(9420160K) icms_dc=7 , 9.8833940 secs] [Times: user=0.42 sys=0.01, real=9.88 secs] 17009.509: [GC 17009.509: [ParNew: 153343K->17023K(153344K), 5.0699960 secs] 6792206K->6727987K(9420160K) icms_dc=7 , 5.0705660 secs] [Times: user=0.55 sys=0.01, real=5.07 secs] 17014.838: [GC 17014.838: [ParNew: 153343K->17023K(153344K), 6.6411750 secs] 6864307K->6790974K(9420160K) icms_dc=7 , 6.6417400 secs] [Times: user=0.37 sys=0.01, real=6.63 secs] 17021.735: [GC 17021.735: [ParNew: 153343K->17024K(153344K), 8.0545970 secs] 6927294K->6856409K(9420160K) icms_dc=7 , 8.0551790 secs] [Times: user=0.34 sys=0.03, real=8.05 secs] 17030.052: [GC 17030.053: [ParNew: 153344K->17023K(153344K), 7.9756730 secs] 6992729K->6922569K(9420160K) icms_dc=7 , 7.9762530 secs] [Times: user=0.34 sys=0.01, real=7.98 secs] 17038.398: [GC 17038.398: [ParNew: 153343K->17022K(153344K), 12.9613300 secs] 7058889K->6990725K(9420160K) icms_dc=7 , 12.9618850 secs] [Times: user=0.39 sys=0.01, real=12.96 secs] 17051.630: [GC 17051.630: [ParNew: 153342K->17022K(153344K), 6.8942910 secs] 7127045K->7059607K(9420160K) icms_dc=7 , 6.8948380 secs] [Times: user=0.56 sys=0.02, real=6.89 secs] 17058.798: [GC 17058.798: [ParNew: 153342K->17024K(153344K), 10.0262190 secs] 7195927K->7126351K(9420160K) icms_dc=7 , 10.0267860 secs] [Times: user=0.37 sys=0.01, real=10.02 secs] 17069.096: [GC 17069.096: [ParNew: 153344K->17023K(153344K), 10.0419500 secs] 7262671K->7195002K(9420160K) icms_dc=7 , 10.0425020 secs] [Times: user=0.40 sys=0.02, real=10.04 secs] 17079.410: [GC 17079.410: [ParNew: 153343K->17022K(153344K), 13.5389040 secs] 7331322K->7264275K(9420160K) icms_dc=7 , 13.5394610 secs] [Times: user=0.30 sys=0.01, real=13.54 secs] 17093.223: [GC 17093.224: [ParNew: 153342K->17023K(153344K), 10.5909450 secs] 7400595K->7330446K(9420160K) icms_dc=7 , 10.5915060 secs] [Times: user=0.33 sys=0.00, real=10.58 secs] 17104.083: [GC 17104.084: [ParNew: 153343K->17024K(153344K), 5.8420210 secs] 7466766K->7392173K(9420160K) icms_dc=7 , 5.8425920 secs] [Times: user=0.57 sys=0.00, real=5.84 secs] 

我花了好几个小时倾注了描述Java GC调优的各种网页,但没有一个真正让我能够解释上面的日志并提出一个行动方案。 根据我提供的日志提出的任何具体建议将不胜感激。

更新:根据以下问题:

该机有16G的RAM,这里是从顶部的信息:内存:15483904k总计,15280084k使用,203820k免费,155684k缓冲交换:2031608k总计,1347240k使用,684368k免费,3304044k缓存

它是一个不同的运行,但这是当前流程的最高输出:

  PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 1016 sanity 17 0 10.2g 6.5g 9464 S 1 44.2 10:24.32 java 

更新2:一些更详细的日志记录,这看起来像是导致了400毫秒的延迟:

 {Heap before GC invocations=1331 (full 1): par new generation total 153344K, used 153343K [0x00002aaaae200000, 0x00002aaab8860000, 0x00002aaab8860000) eden space 136320K, 100% used [0x00002aaaae200000, 0x00002aaab6720000, 0x00002aaab6720000) from space 17024K, 99% used [0x00002aaab77c0000, 0x00002aaab885fff0, 0x00002aaab8860000) to space 17024K, 0% used [0x00002aaab6720000, 0x00002aaab6720000, 0x00002aaab77c0000) concurrent mark-sweep generation total 7169664K, used 4258496K [0x00002aaab8860000, 0x00002aac6e200000, 0x00002aac6e200000) concurrent-mark-sweep perm gen total 21248K, used 13269K [0x00002aac6e200000, 0x00002aac6f6c0000, 0x00002aac73600000) 484.738: [GC 484.738: [ParNew: 153343K->17022K(153344K), 0.3950480 secs] 4411840K->4341689K(7323008K), 0.3954820 secs] [Times: user=0.49 sys=0.07, real=0.40 secs] Heap after GC invocations=1332 (full 1): par new generation total 153344K, used 17022K [0x00002aaaae200000, 0x00002aaab8860000, 0x00002aaab8860000) eden space 136320K, 0% used [0x00002aaaae200000, 0x00002aaaae200000, 0x00002aaab6720000) from space 17024K, 99% used [0x00002aaab6720000, 0x00002aaab77bfb68, 0x00002aaab77c0000) to space 17024K, 0% used [0x00002aaab77c0000, 0x00002aaab77c0000, 0x00002aaab8860000) concurrent mark-sweep generation total 7169664K, used 4324666K [0x00002aaab8860000, 0x00002aac6e200000, 0x00002aac6e200000) concurrent-mark-sweep perm gen total 21248K, used 13269K [0x00002aac6e200000, 0x00002aac6f6c0000, 0x00002aac73600000) } 

原来问题是堆被换出到磁盘,延迟是Java的GC不得不等待它被重新交换。

通过将Linux的“swappiness”参数设置为0来解决(大部分)问题。

从时代来看,似乎GC实际上并不是一直运行(参见用户时间),所以大多数时候,它都在等待。

只是一个疯狂的猜测:它不交换? 机器有多少内存? java进程有多少(驻留集大小)?

编辑:为什么它在等待:

看看这个(从你的成绩单)

 [Times: user=0.39 sys=0.01, real=12.96 secs] 

这意味着(我猜从GC的开始到结束),12(几乎13)秒过去了。 在这13秒中,.39用于在用户模式下运行,.01用于在内核模式下运行。 如果时间收集方法没有完全缺陷(即数字确实代表GC进程/线程的运行时间),这意味着等待至少12秒。

一般来说,当你需要这么大的堆时,很难让GC调整正确。

话虽这么说,大多数GC挂起时间都是由在年轻垃圾收集中存活的对象的内存复制引起的。

您的ConcurrentLinkedHashMap是否同时使用所有内存持久对象进行初始化? 或者它随着应用程序的运行而逐渐变大? 如果是后者,可能很难减少GC挂起时间,因为有些物体总能存活下来。 如果是前者,则需要根据持久对象的大小+ 20%左右来确定您的终身代的大小,并确保您的年轻代足够大以跟上在过程中创建的瞬态对象。你的申请。

也许200ms的性能限制太严格了,你需要自己管理垃圾收集? 你有没有尝试过更大的限制?

我想你有这个UseConcMarkSweepGC和NewRatio错误 。 因为你的新空间不可能接近-Jmx=9G十分之一。 该错误包括一个解决方法(绝对大小的NewSize)。

另一个可能对您非常重要的标志是CMSInitiatingOccupancyFraction 。 它在java6中设置为92%,在java5中设置为68%。 如果旧空间变大,CMS线程池将开始工作。 如果你有CPU花费,那么拥有一个高于初始分数的live-set就没有危险。

如果在修复内存分页问题后包含了GC统计信息,那将会很好。

如果您有严格的时序要求,也许您应该检查Java实时系统 。

RTSJ / Java RTS提供:

API集,语义Java VM增强和JVM到OS层修改,允许Java开发人员正确推理和控制Java应用程序的时间行为。

您是否通过分析器运行您的应用程序,看看您认为正在发生的事情是实际发生的事情?

我在研究这个问题时要做的一件事就是使用Netbeans分析器(尽管任何分析器都应该给你)是查看实时字节(内存分配)并查看那些具有大量分配字节的内容和分配对象是我期待的,并且来自我期望的地方。

您也可以使用探查器查看调整所产生的效果,运行应用程序而不需要任何参数,然后添加您的调优参数并再次运行它,看看内存发生了什么。

如果没有实际看到,并且在某些情况下分析代码,可能很难分辨。

您是否为任何对象实现了finalize()? 这将导致GC大的惩罚。 看到一个可能有6个Gigs的测试运行也是很有趣的,如果你的性能得到不成比例的改善,那就表明GC正在为内存挣扎。

你可以发布/链接到ConcurrentLinkedHashMap实现的代码吗? 如果这是我发布的实现,请在项目页面上打开一张票,这样我们就可以一起调试它。 如果没有,了解实施细节将有助于确定问题所在。

9 GB JVM! 从来没见过! 我想你的10秒。 停顿是很正常的。 看到这个 (也许你已经读过了……)

对我来说问题是完整的幸存者空间。 由于CMS不是压缩的,垃圾被直接提升为旧的。 如此庞大的堆可能会非常昂贵。 有必要增加幸存者空间和MaxTenuringThreshold以尽可能避免升级。

亚历克斯

我认为你的注意力可能有点误导。

花一点时间在一个分析器中找到你最大的分配热点。 如果您的代码中只有少数位置发生了大部分分配,请尝试使用对象池而不是始终构造新对象。

集合类和StringBuilder非常适合池化。 当您将它们返回到池中时,请调用collection.clear()或stringbuilder.setLength(0)方法,以便在下一个调用者想要从池中检索它们时准备好使用它们。

调整GC的最佳方法是创建更少的对象。 有很多策略可以消除分配,而汇集只是其中之一(尽管我最喜欢的一个)。

更新:我写这个答案已经五年了,我对汇集的看法已基本改变了。 回到2009年我写这个答案的时候,我经常可以使用对象池(甚至像StringBuilder这样的简单对象)来加速紧密的内部循环和大量的分配。 现在,更难找到汇集不会使情况变得更糟的情况。 除了线程或连接之外,我几乎从不使用池。 尽管如此,即使您不经常使用它,它也是一个很好的工具。