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堆大小的自適應能力