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

我在一台具有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内存缓存占用的。

任何帮助或建议将不胜感激。

首先,查看Java SE 6 HotSpot [tm]虚拟机垃圾收集调整文档,如果您还没有这样做的话。 该文档说:

并发收集器在应用程序线程仍在运行时执行大部分跟踪和扫描工作,因此应用程序线程只能看到短暂的暂停。 但是,如果并发收集器无法在终端生成填满之前完成对无法访问的对象的回收,或者如果无法使用tenured generation中的可用空闲块满足分配,则应用程序将暂停并且收集完成所有应用程序线程都停止了 无法同时完成收集称为并发模式失败,表示需要调整并发收集器参数。

稍后……

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

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

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

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

祝你好运。

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

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

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

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

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

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

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

我还建议使用GCViewer和分析器。

一些地方开始寻找:

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

我希望可以提供一些帮助:

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

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

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

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

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

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

我没有亲自使用如此庞大的堆,但我通常使用以下用于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备忘单 。