Java虛擬機學習筆記(四):內存分配與回收策略

Java技術體系中所提倡的自動內存管理最終可以歸結爲自動化地解決了兩個問題:給對象分配內存以及回收分配給對象的內存

對象的內存分配,往大方向講,就是在堆上分配,對象主要分配在新生代的Eden區上,如果啓動了本地線程分配緩衝,將按線程優先在TLAB上分配。少數情況下也可能會直接分配在老年代中,分配的規則並不是百分之百固定的,其細節取決於當前使用的是哪一種垃圾收集器組合,還有虛擬機中與內存相關的參數設置。

下面以Serial/Serial Old收集器爲例,介紹幾條最普遍的內存分配規則。

對象優先在Eden分配

大多數情況下,對象在新生代Eden區中分配。當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC。

虛擬機提供了-XX:+PrintGCDetails這個收集器日誌參數,告訴虛擬機在發生垃圾收集行爲時打印內存回收日誌,並且在進程退出的時候輸出當前的內存各區域分配情況。

在實際應用中,內存回收日誌一般是打印到文件後通過日誌工具進行分析

public class Demo1 {

	private static final int _1MB = 1024*1024;
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		byte[] allocation1,allocation2,allocation3,allocation4;
		allocation1 = new byte[2*_1MB];
		allocation2 = new byte[2*_1MB];
		allocation3 = new byte[2*_1MB];
		allocation4 = new byte[4*_1MB];
	}
}

以上Java代碼運行的虛擬機參數爲:

-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8

以上參數的意義爲限制Java堆大小爲20M,不可擴展,其中10M分配給新生代,剩餘10M給老年代。Eden區和一個Survivor區的空間比例是8:1。以下是運行的結果:

Eden
結果顯示新生代用了4M,老年代用了6M,原因是在前三個分配6M,新生代是足夠的,但是當第四個分配4M的時候,不夠分配,會產生一次Minor GC,因爲剩下的那一個Survivor不夠保存已有的三個對象,所以通過分配擔保機制這6M提前轉移至老年代。

新生代GC(Minor GC):指發生在新生代的垃圾收集動作
老年代GC(Major GC/Full GC):指發生在老年代的GC。Full GC的速度一般會比Minor GC慢10倍以上

大對象直接進入老年區

所謂的大對象是指,需要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組(上例中的byte[] 數組就是典型的大對象)。經常出現大對象容易導致內存還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來“安置”它們。

虛擬機提供了一個-XX:PretenureSizeThreshold參數,令大於這個設置值的對象直接在老年代分配。這樣做的目的是避免在Eden區以及兩個Survivor區之間發生大量的內存複製。

以下是示例代碼:

public class Demo1 {

	private static final int _1MB = 1024*1024;
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		byte[] allocation;
		allocation = new byte[4*_1MB];
	}
}

下面是設置的虛擬機參數:

-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728

除了上例的條件,這裏還限制了直接分配到老年代的閾值。以下是運行的結果:
大對象

長期存活的對象將進入老年區

既然虛擬機採用了分代收集的思想來管理內存,那麼必然就提供了識別哪些對象放在新生代,哪些對象放在老年代的工具,那就是對象年齡計數器如果對象在Eden出生並經過第一次Minor GC後仍然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,並且對象年齡設爲1。對象在Survivor區中每“熬過”一次Minor GC,年齡就增加1歲,當他的年齡增加到一定程度(默認15歲),就將會被晉升到老年代中。對象晉升老年代的年齡閾值,可以通過參數-XX:MaxTenuringThreshold設置

以下是示例代碼:

public class Demo1 {

	private static final int _1MB = 1024*1024;
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		@SuppressWarnings("unused")
		byte[] allocation1,allocation2,allocation3;
		allocation1 = new byte[_1MB/4];
		allocation2 = new byte[4*_1MB];
		allocation3 = new byte[4*_1MB];
		allocation3 = null;
		allocation3 = new byte[4*_1MB];
	}

}

下面是虛擬機參數:

-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution

以下是運行結果:
對象年齡計數器1
對象年齡計數器2;

動態對象年齡判定

如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡

以下是示例代碼:

public class Demo1 {

	private static final int _1MB = 1024*1024;
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		byte[] allocation1,allocation2,allocation3,allocation4;
		allocation1 = new byte[_1MB/4];
		allocation2 = new byte[_1MB/4];
		allocation3 = new byte[4*_1MB];
		allocation4 = new byte[4*_1MB];
		allocation4 = null;
		allocation4 = new byte[4*_1MB];
	}

}

虛擬機參數參考上例。以下是執行結果:
動態對象年齡判定
動態對象年齡判斷2
當我們註釋掉allocation1時,發現老年代減少了3%,這可以從反面說明上面的結論。

空間分配擔保

在發生Minor GC(新生代回收)之前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果這個條件成立,那麼Minor GC可以確保是安全的。如果不成立,則虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於,將嘗試着進行一次Minor GC,儘管這次Minor GC是有風險的;如果小於,或者HandlePromotionFailure設置不允許冒險,那這時也要改爲進行一次Full GC

HandlePromotionFailure在JDK 6 Update24之後不會再影響到虛擬機的空間分配擔保策略。之後的規則變爲:只要老年代的連續空間大於新生對象總大小或者歷次晉升的平均大小就會進行Minor GC,否則進行Full GC。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章