频繁的主要gc但不是免费堆?

时间:2015-11-19 14:41:46

标签: java garbage-collection

运行几个小时后,我的http服务器频繁开始主要的gc,但没有释放堆。

几次主要gc之后,promotion failedconcurrent mode failure发生,然后堆被释放。我的gc日志如下:

{Heap before GC invocations=7172 (full 720):
 par new generation   total 737280K, used 667492K [0x000000076b800000, 0x000000079d800000, 0x000000079d800000)
  eden space 655360K, 100% used [0x000000076b800000, 0x0000000793800000, 0x0000000793800000)
  from space 81920K,  14% used [0x0000000793800000, 0x00000007943d91d0, 0x0000000798800000)
  to   space 81920K,   0% used [0x0000000798800000, 0x0000000798800000, 0x000000079d800000)
 concurrent mark-sweep generation total 1482752K, used 1479471K [0x000000079d800000, 0x00000007f8000000, 0x00000007f8000000)
 concurrent-mark-sweep perm gen total 131072K, used 58091K [0x00000007f8000000, 0x0000000800000000, 0x0000000800000000)
2015-11-19T21:50:02.692+0800: 113963.532: [GC2015-11-19T21:50:02.692+0800: 113963.532: [ParNew (promotion failed)
Desired survivor size 41943040 bytes, new threshold 15 (max 15)
- age   1:    3826144 bytes,    3826144 total
- age   2:     305696 bytes,    4131840 total
- age   3:     181416 bytes,    4313256 total
- age   4:     940632 bytes,    5253888 total
- age   5:      88368 bytes,    5342256 total
- age   6:     159840 bytes,    5502096 total
- age   7:     733856 bytes,    6235952 total
- age   8:      64712 bytes,    6300664 total
- age   9:     314304 bytes,    6614968 total
- age  10:     587160 bytes,    7202128 total
- age  11:      38728 bytes,    7240856 total
- age  12:     221160 bytes,    7462016 total
- age  13:     648376 bytes,    8110392 total
- age  14:      33296 bytes,    8143688 total
- age  15:     380768 bytes,    8524456 total
: 667492K->665908K(737280K), 0.7665810 secs]2015-11-19T21:50:03.459+0800: 113964.299: [CMS2015-11-19T21:50:05.161+0800: 113966.001: [CMS-concurrent-mark: 3.579/4.747 secs] [Times: user=13.41 sys=0.35, rea
l=4.75 secs] 
 (concurrent mode failure): 1479910K->44010K(1482752K), 4.7267420 secs] 2146964K->44010K(2220032K), [CMS Perm : 58091K->57795K(131072K)], 5.4939440 secs] [Times: user=9.07 sys=0.13, real=5.49 secs] 
Heap after GC invocations=7173 (full 721):
 par new generation   total 737280K, used 0K [0x000000076b800000, 0x000000079d800000, 0x000000079d800000)
  eden space 655360K,   0% used [0x000000076b800000, 0x000000076b800000, 0x0000000793800000)
  from space 81920K,   0% used [0x0000000798800000, 0x0000000798800000, 0x000000079d800000)
  to   space 81920K,   0% used [0x0000000793800000, 0x0000000793800000, 0x0000000798800000)
 concurrent mark-sweep generation total 1482752K, used 44010K [0x000000079d800000, 0x00000007f8000000, 0x00000007f8000000)
 concurrent-mark-sweep perm gen total 131072K, used 57795K [0x00000007f8000000, 0x0000000800000000, 0x0000000800000000)
}  

似乎CMS GC没有任何意义。 你能解释一下吗?

这是我的gc配置:

/usr/local/jdk1.7.0_79/bin/java 
-server 
-Xms2248m 
-Xmx2248m 
-Xmn800m 
-XX:PermSize=128m 
-XX:MaxPermSize=128m 
-XX:MaxTenuringThreshold=15 
-XX:+UseCMSCompactAtFullCollection 
-XX:CMSFullGCsBeforeCompaction=0 
-XX:+UseConcMarkSweepGC 
-XX:+PrintGCDetails 
-XX:+PrintGCTimeStamps 
-XX:+PrintGCDateStamps 
-Xloggc:gc.log 
-XX:+PrintHeapAtGC 
-XX:+PrintTenuringDistribution 
-XX:+UseFastAccessorMethods

更新

自服务器启动以来,有一个定期任务。它的工作是从mysql加载数据并保存在jvm堆中。当客户端请求到来时,服务器应该使用数据进行计算。像这样的任务代码:

private volatile List<ActivityInfo> activityInfos;

public void run () {
    activityInfos = db.loadActivity();
}

public ActivityInfo getActivityByClient() {
    //
    List<ActivityInfo> local = activityInfos; 
    // biz code
    ActivityInfo response = // biz code
    return response;
}

// executor 
executor.scheduleWithFixedDelay(task, 0, 1, TimeUnit.MINUTES);

让我最困惑的是为什么在完全gc之后释放堆,而不是在主要的gc之后?

更新

full gc log is here

3 个答案:

答案 0 :(得分:4)

这表明你的运行非常接近你的最大堆大小,因此有频繁的GC但很少被释放。尝试显着增加它,比如增加1.5倍或2倍。

答案 1 :(得分:4)

要解决此问题,您可以使用 Eclipse Memory Analyzer 。它将详细向您展示所有这些内存和线程相关的问题。 你也可以 jConsole

答案 2 :(得分:3)

使用-Xmn800m将托儿所堆设置为800MB,但收集后的托儿所堆使用量仅为8MB

- age 15: 380768 bytes, 8524456 total

所以你的应用程序可以运行很长时间只是垃圾收集托儿所堆。然而,在某些时候,终身堆将填满。通过集合7172,只剩下大约3MB - 总值和使用的堆值之间的差异。

concurrent mark-sweep generation total 1482752K, used 1479471K

垃圾收集器已经注意到,tenured heap接近容量,并且在发布日志活动开始之前将触发并发标记。在并发标记阶段,继续分配对象,并且托儿所堆填满触发托儿所集合。

当收集托儿所时,终身堆中没有足够的空间来容纳要提升到终身堆的对象。这导致promotion failed并且垃圾收集器被迫处理整个堆,而不仅仅是托儿所。由于这发生在并发标记阶段完成之前,还会记录concurrent mode failure。完全收集后,终身堆中有1.4GB。

concurrent mark-sweep generation total 1482752K, used 44010K

这是事情发生的方式。如果大多数新物品快速超出范围,幼儿园收藏品便宜,而JVM只会尽可能地收集幼儿园。最终虽然终身堆填满了,但需要更昂贵的完整GC。

如果你减少了托儿所,比如一半大小,会怎么样?假设您的应用程序以恒定速率创建对象,则托儿所将填充大约一半的时间。由于应用程序使用的数据量与GC无关,因此与较大的托儿所一样,将保留和提升相同数量的对象。因此,终身收藏也会更快填满。

但是总堆大小相同。终身区域比以前更大,因此需要更多的托儿所收藏来填补终身区域,因此需要权衡利弊。一个好的经验法则是将托儿所的大小设置为终身区域的四分之一。

<强>更新

完整的gc.log来自不同的GC运行,但我猜应用程序行为类似。在其中我看到很多CMS: abort preclean due to time条消息。这些在Jon Masamitsu's Weblog中描述。出于提高效率的原因,CMS收集器依赖于托儿所集合在停止所有可执行线程之前发生。如果在一定时间内没有发生此类收集,则CMS集合将中止。

当应用程序负载较低,但终端堆使用率较高时,CMS收集器将开始运行并经历其初始标记阶段。当托儿所集合无法运行时,CMS集合将中止。这可能会发生几次。然后发生一个托儿所堆,循环重复。这将继续进行,直到CMS和托儿所收集重合,或者终身堆完全填满。

由于对象只是缓慢地提升到终端堆,这种行为可能会持续一段时间。这里持续时间为2015-11-24T00:28:23.921至2015-11-24T01:55:52.461 - 一个半小时。在此期间,浪费时间执行初始标记仅中止操作。

有许多方法可以解决此问题。

  • 减少托儿所的大小。这将提高托儿所收藏率,因此它们将更频繁地与CMS收藏重合。然而,大型托儿所似乎运作良好,减少它将导致更多CMS集合在重负载下性能更差。
  • 增加CMSMaxAbortablePrecleanTime。这意味着CMS将在中止收集之前等待更长时间。但是等待时间越长,CMS集合就越贵
  • 启用CMSScavengeBeforeRemark。这是我的建议。这将在恰当的时间强制进行托儿所收集,CMS集合永远不会中止。由于托儿所收集也会发生,因此评论阶段的暂停时间会更长,但由于额外时间很短而且完整收藏非常罕见,因此这不太可能成为问题。

请注意,托儿所堆有时被称为年轻一代,而终身堆则是老一代。有关详细信息,请查看Understanding Garbage CollectionGarbage Collection Basics