JVM堆大小的自适应能力

分享到:

在完善我们的测试台以便提高Plumbr排查GC故障能力的时候,我编写了一个小小的测试用例,我觉得应该会有不少人对它感兴趣。我的目标是测试JVM在不同的伊甸区(Eden), 存活区(Survivor)以及年老代空间的分配情况下的自适应能力。

这个测试用例就是在批量地生成对象。每秒会批量生成一批,每批大概是500KB的大小。这些对象的生命周期是5秒钟,之后它们的引用会被删除掉,然后就可以进行垃圾回收了。

本次测试是运行在Mac OS X的Oracle Hotspot 7 JVM上的,使用的是ParallelGC策略,堆的大小是30M。知道了运行的平台之后,我们可以断定出JVM会按下面的堆配置进行启动:

  • 年轻代大小10M,年老代20M,由于没有显式地指定堆的分配比例,JVM默认会按1:2的比例来划分年轻代和年老代的堆空间。
  • 在我的Mac OS X上,10MB的年轻代又会进一步划分为伊甸区和两个存活区,分别是8MB和2*1MB。再强调一遍,这些默认值都是和特定平台相关的。

启动测试用例并通过jstat查看了GC的详细情况后,事实证明我们刚才的预测是正确的:

My Precious:gc-pressure me$ jstat -gc 2533 1s
 S0C
S1C
S0U
S1U
EC
EU
OC
OU
PC
PU
YGC
YGCT
FGC
FGCT
GCT
1024.0 1024.0  0.0
0.0
8192.0   5154.4   20480.0
0.0
21504.0 2718.9
0
0.000   0
0.000
0.000
1024.0 1024.0  0.0
0.0
8192.0   5502.1   20480.0
0.0
21504.0 2720.1
0
0.000   0
0.000
0.000
1024.0 1024.0  0.0
0.0
8192.0   6197.5   20480.0
0.0
21504.0 2721.0
0
0.000   0
0.000
0.000
1024.0 1024.0  0.0
0.0
8192.0   6545.2   20480.0
0.0
21504.0 2721.2
0
0.000   0
0.000
0.000
1024.0 1024.0  0.0
0.0
8192.0   7066.8   20480.0
0.0
21504.0 2721.6
0
0.000   0
0.000
0.000
1024.0 1024.0  0.0
0.0
8192.0   7588.3   20480.0
0.0
21504.0 2722.1
0
0.000   0
0.000
0.000

现在我们还可以继续预测一下接下来会发生什么:

  • 伊甸区的8MB会在16秒内填充满——记住,我们每秒会生成500KB的对象。
  • 任何时刻都会有正好2.5MB的存活对象——每秒生成500KB并持有这些对象5秒钟,最后就是这个数
  • 伊甸区占满的时候触发年轻代GC(Minor GC)——也就是说每16秒会出现一次年轻代的GC
  • 年轻代GC后,会出现过早提升(Premature promotion,注:这些对象过早地提升到了年老代)——存活区只有1MB的大小,而存活对象有2.5MB,1MB的存活区无法容纳这些对象。因此唯 一的途径就是将存活区存放不下的1.5MB(2.5MB-1MB)的对象移动到年老代里。

查看下日志可以证实我的这个猜想:

My Precious:gc-pressure me$ jstat -gc -t 2575 1s
Time   S0C
S1C
S0U
S1U
EC
EU
OC
OU
PC
PU
YGC
YGCT
FGC
FGCT
GCT
6.6 1024.0 1024.0  0.0
0.0
8192.0   4117.9   20480.0
0.0
21504.0 2718.4
0
0.000   0
0.000
0.000
7.6 1024.0 1024.0  0.0
0.0
8192.0   4639.4   20480.0
0.0
21504.0 2718.7
0
0.000   0
0.000
0.000
... cut for brevity ...
 14.7 1024.0 1024.0  0.0
0.0
8192.0   8192.0   20480.0
0.0
21504.0 2723.6
0
0.000   0
0.000
0.000
 15.6 1024.0 1024.0  0.0   1008.0  8192.0   963.4
20480.0
1858.7   21504.0 2726.5
1
0.003   0
0.000
0.003
 16.7 1024.0 1024.0  0.0   1008.0  8192.0   1475.6   20480.0
1858.7   21504.0 2728.4
1
0.003   0
0.000
0.003
... cut for brevity ...
 29.7 1024.0 1024.0  0.0   1008.0  8192.0   8163.4   20480.0
1858.7   21504.0 2732.3
1
0.003   0
0.000
0.003
 30.7 1024.0 1024.0 1008.0  0.0
8192.0   343.3
20480.0
3541.3   21504.0 2733.0
2
0.005   0
0.000
0.005
 31.8 1024.0 1024.0 1008.0  0.0
8192.0   952.1
20480.0
3541.3   21504.0 2733.0
2
0.005   0
0.000
0.005
... cut for brevity ...
 45.8 1024.0 1024.0 1008.0  0.0
8192.0   8013.5   20480.0
3541.3   21504.0 2745.5
2
0.005   0
0.000
0.005
 46.8 1024.0 1024.0  0.0   1024.0  8192.0   413.4
20480.0
5201.9   21504.0 2745.5
3
0.008   0
0.000
0.008
 47.8 1024.0 1024.0  0.0   1024.0  8192.0   961.3
20480.0
5201.9   21504.0 2745.5
3
0.008   0

触发垃圾回收触发的时间是15秒左右,而不是16秒,它会清理掉伊甸区并将大约1MB的对象移动到存活区,同时将剩余的那些对象移动到年老代。

目前为止一切都和我们猜测的一样。JVM的确是按我们所理解的方式来运行的。有趣的是一旦JVM监控到GC的行为并知道发生的情况之后。在我们这个测试用例中,这个发生在90秒左右的时候:

My Precious:gc-pressure me$ jstat -gc -t 2575 1s
Time   S0C
S1C
S0U
S1U
EC
EU
OC
OU
PC
PU
YGC
YGCT
FGC
FGCT
GCT
94.0 1024.0 1024.0  0.0   1024.0  8192.0   8036.8   20480.0
8497.0   21504.0 2748.8
5
0.012   0
0.000
0.012
 95.0 1024.0 3072.0 1024.0  0.0
4096.0   353.3
20480.0
10149.6   21504.0 2748.8
6
0.014   0
0.000
0.014
 96.0 1024.0 3072.0 1024.0  0.0
4096.0   836.6
20480.0
10149.6   21504.0 2748.8
6
0.014   0
0.000
0.014
 97.0 1024.0 3072.0 1024.0  0.0
4096.0   1350.0   20480.0
10149.6   21504.0 2748.8
6
0.014   0
0.000
0.014
 98.0 1024.0 3072.0 1024.0  0.0
4096.0   1883.5   20480.0
10149.6   21504.0 2748.8
6
0.014   0
0.000
0.014
 99.0 1024.0 3072.0 1024.0  0.0
4096.0   2366.8   20480.0
10149.6   21504.0 2748.8
6
0.014   0
0.000
0.014
100.0 1024.0 3072.0 1024.0  0.0
4096.0   2890.2   20480.0
10149.6   21504.0 2748.8
6
0.014   0
0.000
0.014
101.0 1024.0 3072.0 1024.0  0.0
4096.0   3383.7   20480.0
10149.6   21504.0 2748.8
6
0.014   0
0.000
0.014
102.0 1024.0 3072.0 1024.0  0.0
4096.0   3909.7   20480.0
10149.6   21504.0 2748.8
6
0.014   0
0.000
0.014
103.0 3072.0 3072.0  0.0   2720.0  4096.0   323.0
20480.0
10269.6   21504.0 2748.9
7
0.016   0
0.000
0.016

我们可以看到JVM的强大的适能力。一旦了解了应用的行为之后,JVM会将存活区调整为足够容纳下所有的存活对象。现在年轻代的配置变成了这样:

  • 伊甸区4MB
  • 存活区每个3MB

调整之后GC的频率会有所提高——伊甸区现在只有原来的一半了,因此现在是每8秒一次GC而不是原来的16秒。不过好处也是显而易见的,现在的存活 区已经足以存放所有的存活对象了。考虑到已经没有对象能活过一次年轻代GC的周期(不过还得记住,任何时刻都仍有2.5MB的存活对象),因此也不用再将 对象提升到年老代中了。

继续观察JVM后我们会发现年老代的使用率在调整后会一直保持不变。对象不会再移动到年老代中了,不过由于也不会再触发年老代GC,所以在JVM调整前提升进来的那10MB的垃圾对象就会一直存放在年老代里面了。

你也可以把JVM的这个“神奇的自适应性”的功能给屏蔽掉,如果你确定要这么做的话。在JVM的参数中指定-XX- UseAdaptiveSizingPolicy就会让JVM严格遵循启动时所指定的参数,而不会违背你的旨意。请谨慎使用这一选项,现代的JVM通常都 能自动地给你预测出恰当的配置。

原创文章转载请注明出处:JVM堆大小的自适应能力

英文原文链接

昵    称:
验证码:

相关文档: