<减少JVM暂停时间>使用UseConcMarkSweepGC 1秒

时间:2009-02-22 00:29:46

标签: java performance garbage-collection jvm jvm-arguments

我在拥有16Gb RAM,8核处理器和Java 1.6的计算机上运行内存密集型应用程序,所有这些都运行在CentOS 5.2版(最终版)上。精确的JVM详细信息包括:

java version "1.6.0_10"
Java(TM) SE Runtime Environment (build 1.6.0_10-b33)
Java HotSpot(TM) 64-Bit Server VM (build 11.0-b15, mixed mode)

我正在使用以下命令行选项启动应用程序:

java -XX:+UseConcMarkSweepGC -verbose:gc -server -Xmx10g -Xms10g ...

我的应用程序公开了一个JSON-RPC API,我的目标是在25ms内响应请求。不幸的是,我看到延迟超过1秒,似乎是垃圾收集造成的。以下是一些较长的例子:

[GC 4592788K->4462162K(10468736K), 1.3606660 secs]
[GC 5881547K->5768559K(10468736K), 1.2559860 secs]
[GC 6045823K->5914115K(10468736K), 1.3250050 secs]

这些垃圾收集事件中的每一个都伴随着延迟的API响应,其持续时间与显示的垃圾收集长度非常相似(在几毫秒内)。

以下是一些典型示例(这些示例均在几秒钟内完成):

[GC 3373764K->3336654K(10468736K), 0.6677560 secs]
[GC 3472974K->3427592K(10468736K), 0.5059650 secs]
[GC 3563912K->3517273K(10468736K), 0.6844440 secs]
[GC 3622292K->3589011K(10468736K), 0.4528480 secs]

问题是我认为UseConcMarkSweepGC可以避免这种情况,或者至少使它非常罕见。相反,超过100毫秒的延迟几乎每分钟发生一次或更多(尽管超过1秒的延迟相当罕见,可能每10或15分钟一次)。

另一件事是,我认为只有一个FULL GC会导致线程被暂停,但这些似乎不是完整的GC。

可能需要注意的是,大多数内存都是由使用软引用的LRU内存缓存占用。

非常感谢任何帮助或建议。

8 个答案:

答案 0 :(得分:11)

首先,查看Java SE 6 HotSpot[tm] Virtual Machine Garbage Collection Tuning文档,如果您还没有这样做的话。该文档说:

  

并发收集器完成了大部分跟踪和扫描工作   应用程序线程仍在运行,因此只能看到短暂的暂停   应用程序线程但是,如果并发收集器无法完成   在终生代填满之前回收无法到达的对象,或者如果   无法满足分配中可用的空闲空间块   终身生成,然后暂停应用程序并完成收集   所有应用程序线程都停止了。无法完成收藏   并发被称为并发模式失败并表明需要   调整并发收集器参数。

稍后......

  

并发收集器在a期间暂停应用程序两次   并发收集周期。

我注意到那些GC似乎没有释放太多内存。也许你的许多物品都是长寿的?您可能希望调整生成大小和其他GC参数。 10 Gig是许多标准的巨大的堆,我天真地期望GC在这么大的堆上花费更长的时间。仍然,1秒是一个非常长的暂停时间,并指示出现问题(您的程序正在生成大量不需要的对象或生成难以回收的对象,或其他东西)或者您只需要调整GC。

通常,我会告诉别人如果他们必须调整GC,那么他们还有其他需要先解决的问题。但是对于这种规模的应用,我认为你会陷入“需要比普通程序员更多地了解GC。”

正如其他人所说,您需要分析您的应用程序以查看瓶颈所在。你的PermGen对于分配给它的空间来说太大了吗?你在创造不必要的物品吗? jconsole至少可以显示有关VM的最少信息。这是一个起点。然而,正如其他人指出的那样,您很可能需要比此更高级的工具。

祝你好运。

答案 1 :(得分:11)

既然你提到了缓存的愿望,我猜你的大部分堆都被那个缓存占用了。您可能希望限制缓存的大小,以确保它永远不会尝试增长到足以填充终生代。不要仅依靠SoftReference来限制大小。由于旧一代填充了软引用,旧的引用将被清除并变成垃圾。将创建新的引用(可能是相同的信息),但由于可用空间短缺,因此可以快速清除。最终,终身空间充满垃圾,需要清理。

考虑调整-XX:NewRatio设置。默认值为1:2,表示堆的三分之一分配给新一代。对于大堆,这几乎总是太多。你可能想尝试9这样的东西,它可以为你的老一代保留9 Gb的10 Gb堆。

答案 2 :(得分:6)

原来,堆的一部分被换出到磁盘,因此垃圾收集必须将一堆数据从磁盘中拉回内存。

我通过将Linux的“swappiness”参数设置为0来解决这个问题(这样它就不会将数据交换到磁盘上)。

答案 3 :(得分:2)

以下是我发现的一些可能很重要的事情。

  • JSON-RPC可以生成很多对象。没有XML-RPC那么多,但仍然值得关注。在任何情况下,您似乎每秒生成100 MB的对象,这意味着您的GC运行时间很长,很可能会增加您的随机延迟。即使GC是并发的,您的硬件/操作系统很可能在负载下表现出非理想的随机延迟。
  • 看看你的记忆库架构。在Linux上,命令是numactl --hardware。如果您的VM分布在多个内存库中,这将显着增加GC时间。 (它也会降低你的应用程序的速度,因为这些访问的效率会大大降低)你内存子系统的工作越困难,操作系统就越有可能转移内存(通常是大量的)并且你会因此而出现戏剧性的暂停( 100毫秒并不奇怪)。不要忘记您的操作系统不仅仅是运行您的应用程序。
  • 考虑压缩/减少缓存的内存消耗。如果您使用多GB的缓存,那么值得研究一下如何减少内存消耗。
  • 我建议您同时使用内存分配跟踪和cpu采样来分析您的应用。这会产生非常不同的结果,并且经常指出导致这类问题的原因。

使用这些方法,RPC呼叫的延迟可以降低到低于200微秒,GC时间减少到1-3毫秒,影响不到1/300的呼叫。

答案 4 :(得分:0)

有些地方开始寻找:

此外,我通过分析器运行代码..我喜欢NetBeans中的代码,但也有其他代码。您可以实时查看gc行为。 Visual VM也是这样做的......但我还没有运行它(一直在寻找理由......但还没有时间或需要)。

答案 5 :(得分:0)

我还建议GCViewer和分析器。

答案 6 :(得分:0)

我希望能提供帮助的一些事情:

我从来没有对ConcurrentCollector好运,理论上它牺牲了吞吐量以减少延迟,但我发现吞吐量收集器的吞吐量和延迟(通过调整,以及我的应用程序)。

你的软参考缓存对于分代收藏家来说有点危险,可能是你年轻一代收藏品没有收集太多垃圾的原因之一。

如果我没有弄错的话,无论对象多么短暂,如果它被放入缓存(肯定会进入Tenured Generation),它将一直存在直到FullGC发生,甚至如果没有其他参考文献!

这意味着生活在缓存中的年轻人的对象现在被复制多次,保持活着状态,使他们的引用保持活跃状态​​,并且通常会减慢youngGen GC的速度。

有点矛盾的是,缓存如何减少对象分配,但会增加GC时间。

你可能还想尝试调整你的幸存者比率,它可能太小,甚至更多的“年轻”物体溢出到终身一代。

答案 7 :(得分:0)

我没有亲自使用如此庞大的堆,但我通常使用以下用于Oracle / Sun Java 1.6.x的交换机经历了非常低的延迟:

-Xincgc -XX:+UseConcMarkSweepGC -XX:CMSIncrementalSafetyFactor=50
-XX:+UseParNewGC
-XX:+CMSConcurrentMTEnabled -XX:ConcGCThreads=2 -XX:ParallelGCThreads=2
-XX:CMSIncrementalDutyCycleMin=0 -XX:CMSIncrementalDutyCycle=5
-XX:GCTimeRatio=90 -XX:MaxGCPauseMillis=20 -XX:GCPauseIntervalMillis=1000

在我看来,重要的部分是使用CMS进行终身培养,而ParNewGC用于年轻一代。此外,这为CMS增加了一个非常大的安全系数(默认值为10%而不是50%)并请求短暂的暂停时间。当你的目标是25毫秒的响应时间时,我会尝试将-XX:MaxGCPauseMillis设置为更小的值。您甚至可以尝试使用两个以上的内核用于并发GC,但我会猜测不值得使用CPU。

您可能还应该查看HotSpot JVM GC cheat sheet