Java技術體系中所提倡的自動記憶體管理最終可以歸結為自動化地解決了兩個問題:給對象配置設定記憶體以及回收配置設定給對象的記憶體。
對象的記憶體配置設定,往大方向講,就是在堆上配置設定,對象主要配置設定在新生代的Eden區上,如果啟動了本地線程配置設定緩沖,将按線程優先在TLAB上配置設定。少數情況下也可能會直接配置設定在老年代中,配置設定的規則并不是百分之百固定的,其細節取決于目前使用的是哪一種垃圾收集器組合,還有虛拟機中與記憶體相關的參數的設定。
本文中的記憶體配置設定政策指的是Serial / Serial Old收集器下(ParNew / Serial Old收集器組合的規則也基本一緻)的記憶體配置設定和回收的政策。
對象優先在Eden配置設定
大多數情況下,對象在新生代Eden區中配置設定。當Eden區沒有足夠空間進行配置設定時,虛拟機将發起一次Minor GC。虛拟機的參數
-XX:PrintGCDetails
可以列印記憶體回收日志。
代碼示例:
private static final int _1MB = * ;
/**
* VM參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
*/
public static void testAllocation() {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[ * _1MB];
allocation2 = new byte[ * _1MB];
allocation3 = new byte[ * _1MB];
allocation4 = new byte[ * _1MB]; // 出現一次Minor GC
}
運作結果:
[GC [DefNew: K->K(K), secs] K->K(K), secs] [Times: user= sys=, real= secs]
Heap
def new generation total K, used K [, , )
eden space K, % used [, , )
from space K, % used [, , )
to space K, % used [, , )
tenured generation total K, used K [, , )
the space K, % used [, , , )
compacting perm gen total K, used K [, , )
the space K, % used [, , , )
No shared spaces configured.
代碼嘗試配置設定3個2MB大小和1個4MB大小的對象,在運作時通過-Xms20M、 -Xmx20M、 -Xmn10M這3個參數限制了Java堆大小為20MB,不可擴充,其中10MB配置設定給新生代,剩下的10MB配置設定給老年代。-XX:SurvivorRatio=8決定了新生代中Eden區與一個Survivor區的空間比例是8∶1,從輸出的結果也可以清晰地看到eden space 8192K、from space 1024K、to space 1024K的資訊,新生代總可用空間為9216KB(Eden區+1個Survivor區的總容量)。
執行testAllocation()中配置設定allocation4對象的語句時會發生一次Minor GC,這次GC的結果是新生代6651KB變為148KB,而總記憶體占用量則幾乎沒有減少(因為allocation1、allocation2、allocation3三個對象都是存活的,虛拟機幾乎沒有找到可回收的對象)。這次GC發生的原因是給allocation4配置設定記憶體的時候,發現Eden已經被占用了6MB,剩餘空間已不足以配置設定allocation4所需的4MB記憶體,是以發生Minor GC。GC期間虛拟機又發現已有的3個2MB大小的對象全部無法放入Survivor空間(Survivor空間隻有1MB大小),是以隻好通過配置設定擔保機制提前轉移到老年代去。
這次GC結束後,4MB的allocation4對象順利配置設定在Eden中,是以程式執行完的結果是Eden占用4MB(被allocation4占用),Survivor空閑,老年代被占用6MB(被allocation1、allocation2、allocation3占用)。通過GC日志可以證明這一點。
大對象直接進入老年代
所謂的大對象是指,需要大量連續記憶體空間的Java對象,最典型的大對象就是那種很長的字元串以及數組。
大對象對虛拟機的記憶體配置設定來說就是一個壞消息(比遇到一個大對象更加壞的消息就是遇到一群“朝生夕滅”的“短命大對象”,寫程式的時候應當避免),經常出現大對象容易導緻記憶體還有不少空間時就提前觸發垃圾收集以擷取足夠的連續空間來“安置”它們。
虛拟機提供了一個-XX:PretenureSizeThreshold參數,令大于這個設定值的對象直接在老年代配置設定。這樣做的目的是避免在Eden區及兩個Survivor區之間發生大量的記憶體複制(複習一下:新生代采用複制算法收集記憶體)。
代碼示例:
private static final int _1MB = * ;
/**
* VM參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
* -XX:PretenureSizeThreshold=3145728
*/
public static void testPretenureSizeThreshold() {
byte[] allocation;
allocation = new byte[ * _1MB]; //直接配置設定在老年代中
}
運作結果:
Heap
def new generation total K, used K [, , )
eden space K, % used [, , )
from space K, % used [, , )
to space K, % used [, , )
tenured generation total K, used K [, , )
the space K, % used [, , , )
compacting perm gen total K, used K [, , )
the space K, % used [, , , )
No shared spaces configured.
執行代碼中的testPretenureSizeThreshold()方法後,我們看到Eden空間幾乎沒有被使用,而老年代的10MB空間被使用了40%,也就是4MB的allocation對象直接就配置設定在老年代中,這是因為PretenureSizeThreshold被設定為3MB(就是3145728,這個參數不能像-Xmx之類的參數一樣直接寫3MB),是以超過3MB的對象都會直接在老年代進行配置設定。
注意:PretenureSizeThreshold參數隻對Serial和ParNew兩款收集器有效,Parallel Scavenge收集器不認識這個參數,Parallel Scavenge收集器一般并不需要設定。如果遇到必須使用此參數的場合,可以考慮ParNew加CMS的收集器組合。
長期存活的對象将進入老年代
為了在記憶體回收時能識别哪些對象應放在新生代,哪些對象應放在老年代中。虛拟機給每個對象定義了一個對象年齡(Age)計數器。
對象年齡的判定:
如果對象在Eden出生并經過第一次Minor GC後仍然存活,并且能被Survivor容納的話,将被移動到Survivor空間中,并且對象年齡設為1。對象在Survivor區中每“熬過”一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(預設為15歲),就将會被晉升到老年代中。對象晉升老年代的年齡門檻值,可以通過參數-XX:MaxTenuringThreshold設定。
代碼示例:
private static final int _1MB = * ;
/**
* VM參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1
* -XX:+PrintTenuringDistribution
*/
@SuppressWarnings("unused")
public static void testTenuringThreshold() {
byte[] allocation1, allocation2, allocation3;
allocation1 = new byte[_1MB / ];
// 什麼時候進入老年代取決于XX:MaxTenuringThreshold設定
allocation2 = new byte[ * _1MB];
allocation3 = new byte[ * _1MB];
allocation3 = null;
allocation3 = new byte[ * _1MB];
}
以MaxTenuringThreshold=1參數來運作的結果:
[GC [DefNew
Desired Survivor size bytes, new threshold (max )
- age : bytes, total
: K->K(K), secs] K->K(K), secs] [Times: user= sys=, real= secs]
[GC [DefNew
Desired Survivor size bytes, new threshold (max )
: K->K(K), secs] K->K(K), secs] [Times: user= sys=, real= secs]
Heap
def new generation total K, used K [, , )
eden space K, % used [, , )
from space K, % used [, , )
to space K, % used [, , )
to space K, % used [, , )
to space K, % used [, , )
to space K, % used [, , )
tenured generation total K, used K [, , )
the space K, % used [, , , )
compacting perm gen total K, used K [, , )
the space K, % used [, , , )
No shared spaces configured.
以MaxTenuringThreshold=15參數來運作的結果:
[GC [DefNew
Desired Survivor size bytes, new threshold (max )
- age : bytes, total
: K->K(K), secs] K->K(K), secs] [Times: user= sys=, real= secs]
[GC [DefNew
Desired Survivor size bytes, new threshold (max )
- age : bytes, total
: K->K(K), secs] K->K(K), secs] [Times: user= sys=, real= secs]
Heap
def new generation total K, used K [, , )
eden space K, % used [, , )
from space K, % used [, , )
to space K, % used [, , )
tenured generation total K, used K [, , )
the space K, % used [, , , )
compacting perm gen total K, used K [, , )
the space K, % used [, , , )
No shared spaces configured.
此方法中的allocation1對象需要256KB記憶體,Survivor空間可以容納。當MaxTenuringThreshold=1時,allocation1對象在第二次GC發生時進入老年代,新生代已使用的記憶體GC後非常幹淨地變成0KB。而MaxTenuringThreshold=15時,第二次GC發生後,allocation1對象則還留在新生代Survivor空間,這時新生代仍然有404KB被占用。
動态對象年齡判定
為了能更好地适應不同程式的記憶體狀況,虛拟機并不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代.
動态對象年齡判定:
如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。
代碼示例:
private static final int _1MB = * ;
/**
* VM參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15
* -XX:+PrintTenuringDistribution
*/
@SuppressWarnings("unused")
public static void testTenuringThreshold2() {
byte[] allocation1, allocation2, allocation3, allocation4;
allocation1 = new byte[_1MB / ];
// allocation1+allocation2大于survivo空間一半
allocation2 = new byte[_1MB / ];
allocation3 = new byte[ * _1MB];
allocation4 = new byte[ * _1MB];
allocation4 = null;
allocation4 = new byte[ * _1MB];
}
運作結果:
[GC [DefNew
Desired Survivor size bytes, new threshold (max )
- age : bytes, total
: K->K(K), secs] K->K(K), secs] [Times: user= sys=, real= secs]
[GC [DefNew
Desired Survivor size bytes, new threshold (max )
: K->K(K), secs] K->K(K), secs] [Times: user= sys=, real= secs]
Heap
def new generation total K, used K [, , )
eden space K, % used [, , )
from space K, % used [, , )
to space K, % used [, , )
tenured generation total K, used K [, , )
the space K, % used [, , , )
compacting perm gen total K, used K [, , )
the space K, % used [, , , )
No shared spaces configured.
執行代碼中的testTenuringThreshold2()方法,并設定-XX:MaxTenuringThreshold=15,會發現運作結果中Survivor的空間占用仍然為0%,而老年代比預期增加了6%,也就是說,allocation1、allocation2對象都直接進入了老年代,而沒有等到15歲的臨界年齡。因為這兩個對象加起來已經到達了512KB,并且它們是同年的,滿足同年對象達到Survivor空間的一半規則。我們隻要注釋掉其中一個對象new操作,就會發現另外一個就不會晉升到老年代中去了。
空間配置設定擔保
在發生Minor GC之前,虛拟機會先檢查老年代最大可用的連續空間是否大于新生代所有對象總空間,如果這個條件成立,那麼Minor GC可以確定是安全的。如果不成立,則虛拟機會檢視HandlePromotionFailure設定值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大于曆次晉升到老年代對象的平均大小,如果大于,将嘗試着進行一次Minor GC,盡管這次Minor GC是有風險的;如果小于,或者HandlePromotionFailure設定不允許冒險,那這時也要改為進行一次Full GC。
下面解釋一下“冒險”是冒了什麼風險,前面提到過,新生代使用複制收集算法,但為了記憶體使用率,隻使用其中一個Survivor空間來作為輪換備份,是以當出現大量對象在Minor GC後仍然存活的情況(最極端的情況就是記憶體回收後新生代中所有對象都存活),就需要老年代進行配置設定擔保,把Survivor無法容納的對象直接進入老年代。與生活中的貸款擔保類似,老年代要進行這樣的擔保,前提是老年代本身還有容納這些對象的剩餘空間,一共有多少對象會活下來在實際完成記憶體回收之前是無法明确知道的,是以隻好取之前每一次回收晉升到老年代對象容量的平均大小值作為經驗值,與老年代的剩餘空間進行比較,決定是否進行Full GC來讓老年代騰出更多空間。
取平均值進行比較其實仍然是一種動态機率的手段,也就是說,如果某次Minor GC存活後的對象突增,遠遠高于平均值的話,依然會導緻擔保失敗(Handle Promotion Failure)。如果出現了HandlePromotionFailure失敗,那就隻好在失敗後重新發起一次Full GC。雖然擔保失敗時繞的圈子是最大的,但大部分情況下都還是會将HandlePromotionFailure開關打開,避免Full GC過于頻繁。
參考
1、周志明,深入了解Java虛拟機:JVM進階特性與最佳實踐,機械工業出版社