JVM堆大小的自適應能力

jopen 10年前發布 | 11K 次閱讀 JVM Java開發

在完善我們的測試臺以便提高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堆大小的自適應能力

英文原文鏈接

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!