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
.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
.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
.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
.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
.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
.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 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
.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 ...
.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
.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
.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 ...
.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
.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
.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 ...
.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
.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
.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
.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
.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
.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
.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
.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
.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
.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
.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
.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
.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通常都能自动地给你预测出恰当的配置。