【證】:內存的分配與回收策略

一、名詞解釋

JVM的內存分配及回收策略:

1.對象優先分配到Eden區中;

2.大對象直接進入到老年代;

3.長期存活的對象將進入老年代;

4.動態對象年齡判斷;

5.空間分配擔保機制

JVM的垃圾回收算法採用的分代回收算法,根據對象存活週期不同,將內存分爲年輕代和老年代,這樣可以因地制宜的選擇相應的回收策略。年輕代採用的回收算法是複製算法,瞭解過該算法的童鞋肯定知道,該算法是把內存等分爲二,但是年輕代並沒有用50%的空間來做內存複製空間,因爲根據IBM研究統計,98%的對象都是朝生夕死的,所以改進的分配策略是:一塊比較大的Eden區,兩塊比較小的Survivor區,默認比例是8:1:1,可通過設置SurvivorRatio參數值調整,如SurvivorRatio=3,則Eden:S1:S2=3:1:1,對象優先在Eden區中分配,每次只使用Eden區和其中一個Survivor區,當Eden區沒有足夠的連續空間來分配給新對象時,會觸發一次MinorGC(也有叫youngGC的,是一個意思),也就是年輕代的垃圾回收。未被回收掉的(GCRoots可達,還在使用),會被複制到From Survivor區,此時Eden區就可以被完全釋放,並且,複製到From Survivor的對象會按順序分配內存,這樣便沒有了碎片問題,同時,被複制的對象的age會加1,  當Eden區再次不夠用了,會再次觸發MinorGC,Eden區和Survivor區中還在使用的對象會被複制到To Survivor中,下一次minorGC,Eden和To Survivor區中還在使用的對象則會被複制到From Survivor區中,如此往復,經過若干次minorGC後,總有一些對象在From 和To區之間來回遊蕩,每熬過一次minorGC,對象的age就加一,直到對象的年齡達到一定值(默認爲15,通過MaxTenuringThreshold設置),就會被移動到老年代,這就是長期存活的對象進入老年代,並且,爲了能更好的適應不同場景下的內存情況,JVM並不總是要求對象一定要達到MaxTenuringThreshold才能晉升到老年代,當Survivor區中相同年齡的對象大小總和大於Survivor空間的一半,年齡大於或者等於該年齡的對象就可以直接進入老年代,這就是對象的動態年齡判斷,當出現大量對象在MinorGC後仍然存活,就需要老年代進行分配擔保,讓Survivor無法容納的對象進入到老年代,前提是老年代有足夠的空間來存放這些對象,但是在實際完成內存回收之前JVM是無法知道到底有多少對象要被扔到老年代的,所以只好取之前每一次回收晉升到老年代的對象容量的平均大小作爲經驗值,與老年代的剩餘空間進行比較,決定是否進行Full GC來讓老年代騰出空間,這就是空間分配擔保機制。老年代中對象的存活率極高,且沒有額外的空間進行分配擔保,所以必須使用標記-清除或標記-整理算法進行回收。因爲年輕代使用的是複製算法,如果有大對象分配在Eden區,回收時會出現對大對象的複製操作,比較耗時,且MinorGC的頻率會提高,嚴重影響系統的吞吐量,因此,需要大量連續內存空間的對象,會被直接扔到老年代,最典型的就是那種很長的字符串、數組以及集合,這就是大對象直接進入老年代機制。


二、驗證以上JVM的內存分配及回收策略。

1.對象優先分配到Eden區&&大對象直接進入老年代

/**
 * 測試對象優先分配到Eden區&&大對象直接進入老年代
 * -Xms200m -Xmx200m -Xmn100m -XX:+PrintGCDetails -XX:SurvivorRatio=3 -XX:PretenureSizeThreshold=31m -XX:+UseParNewGC
 * 堆總大小200m,新生代總大小100m,Eden60m,s1及s2均爲20m ,對象大於等於31m直接進入老年代
 * 注意:網上查閱資料發現,描述的PretenureSizeThreshold參數的單位都是字節,但是實際測試過程中發現,配成m,會自動轉換爲字節,如此處配成31m
 * 打印出來的 PretenureSizeThreshold = 32505856;此外PretenureSizeThreshold參數對Parallel Scavenge收集器是無效,此處配置的是ParNew收集器
 * @author ljl
 */
public class TestEdenToAnther {
	private static int _1MB = 1 * 1024 * 1024;
	public static void main(String[] args) throws InterruptedException {
                //斷點處
		byte[] t1 = new byte[30 *_1MB];
		byte[] t2 = new byte[10 *_1MB];
		byte[] t3 = new byte[15 *_1MB];
		byte[] t4 = new byte[45 *_1MB];
 	}

}


debug並結合jvisualvm:

初始狀態,Eden區被佔用了14.417M:


執行byte[] t1 = new byte[30*_1MB];後,t1被分配到Eden區,Eden空間此時被佔用45.618M:


執行byte[] t2 = new byte[10*_1MB];後,t2被分配到Eden區,Eden空間此時被佔用56.819M:

執行byte[] t3 = new byte[15*_1MB];Eden空間不足,觸發MinorGC,t1(30M)大於Survivor區大小,被轉移到老年代,t2(10M)小於Survivor區大小,被複制到Survivor1,Eden騰出空間,t3得以被分配到Eden:

執行byte[] t4 = new byte[45*_1MB];t4大於PretenureSizeThreshold設置的31m,被直接分配到老年代,至此,Eden存放了t3(15M),Survivor存放t2(10MB),老年代最終存放t1(30MB),t4(45MB),如下圖所示:



2.長期存活的對象進入老年代

/**
 * 測試長期存活的對象進入老年代
 * -Xms200m -Xmx200m -Xmn100m -XX:+PrintGCDetails -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution -XX:+UseParNewGC
 * 堆總大小200m,新生代總大小100m,Eden80m,s1及s2均爲10m ,MaxTenuringThreshold=1對象年齡大於1則進入老年代
 * @author ljl
 */
public class TestEdenToAnther {
	private static int _1MB = 1 * 1024 * 1024;

	public static void main(String[] args) throws InterruptedException {
		
		byte[] a = new byte[4 * _1MB];
		byte[] b = new byte[40 * _1MB];
		byte[] c = new byte[41 * _1MB];
		c = null;
		c = new byte[41 * _1MB];
 	}

}

debug並結合jvisualvm:

初始狀態,Eden區被佔用17.601M:

執行byte[] a = new byte[4 * _1MB]; byte[] b = new byte[40 * _1MB];a,b被分配到Eden:

執行byte[] c = new byte[41 * _1MB];觸發MinorGC,a被複制到Survivor,且a對象的age=1,b到老年代,c得以分配到Eden:

執行c = null; c = new byte[41 * _1MB];觸發MinorGC,c原引用對象被完全回收,新引用對象(41M)分配到Eden,a對象age=2>1,被轉移到老年代,至此,Eden存放了c(41M)對象,Survivor被清空,Old Gen存放了a(4M),b(40M)對象:


3.對象動態年齡判斷

/**
 * 測試對象動態年齡判斷
 * -Xms200m -Xmx200m -Xmn100m -XX:+PrintGCDetails -XX:MaxTenuringThreshold=14 -XX:+PrintTenuringDistribution -XX:+UseParNewGC
 * 堆總大小200m,新生代總大小100m,Eden:S1:S2用的默認比例,8:1:1,Eden區80m,S1,S2各10m,配置對象年齡大於14則進入老年代
 *
 * @author ljl
 */
public class TestEdenToAnther {
	private static int _1MB = 1 * 1024 * 1024;
	public static void main(String[] args) throws InterruptedException {

		 byte[] a = new byte[2 * _1MB];
		 byte[] a_temp01 = new byte[3 * _1MB];
		 byte[] a_temp02 = new byte[1 * _1MB];
		 byte[] b = new byte[40 * _1MB];
		 byte[] c = new byte[41 * _1MB];
		 c = null;
		 c = new byte[41 * _1MB];
	}

}

debug並結合jvisualvm:

初始化狀態,Eden區被佔用17.601M:


執行 byte[] a = new byte[2 * _1MB]; byte[] a_temp01 = new byte[3 * _1MB];byte[] a_temp02 = new byte[1 * _1MB];byte[] b = new byte[40 * _1MB];


執行byte[] c = new byte[41 * _1MB];觸發MinorGC,a,a_temp01,a_temp02被複制到Survivor,且三個對象的age均爲1,b到Old Gen,c到Eden:


執行c = null; c = new byte[41 * _1MB];觸發MinorGC,a,a_temp01,a_temp02三個對象的age均加1,變爲2,且三個對象大小之和等於6M,大於Survivor的一半,遂被從Survivor1轉移到Old Gen,c新對象分配到Eden,至此,Eden存放了c(41M)對象,Survivor被清空,Old Gen存放了a,a_temp01,a_temp02,b四個對象:




4.擔保機制

這裏再複習擔保的具體做法:JVM在MinorGC前會先檢查新生代對象總大小是否小於老年代剩餘空間,小於則只MinorGC,大於則檢查是否允許擔保失敗(HandlePromotionFailure是否爲true),如果允許,則繼續檢查老年代剩餘空間是否大於之前每次晉升到老年代的對象平均大小,大於則只進行MinorGC,小於則進行Full GC;如果不允許,則直接進行Full GC.另外,參數HandlePromotionFailure在6.0_24 及其之後的版本被移除掉了,也就是說,後面的版本參數HandlePromotionFailure默認爲true,且不接受配置。筆者的JDK版本爲1.7.0_79,後面也會測試下,是否可修改HandlePromotionFailure爲false.


擔保情景一:晉升到老年代的對象平均大小大於老年代剩餘空間

/**
 * 測試動態年齡判斷
 * -Xms100m -Xmx100m -Xmn50m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=30m -XX:+UseParNewGC -XX:+PrintFlagsFinal
 * 堆總大小100m,新生代總大小50m,Eden:S1:S2用的默認比例,8:1:1,Eden區40m,S1,S2各5m,避免大對象直接進老年代的干擾,把PretenureSizeThreshold配成30m
 * @author ljl
 */
public class TestEdenToAnther {
	private static int _1MB = 1*1024*1024;
	public static void main(String[] args) throws InterruptedException {
		 byte[] a = new byte[10 * _1MB];
		 byte[] b = new byte[10 * _1MB];
		 byte[] c = new byte[15 * _1MB];
		 byte[] d = new byte[10 * _1MB];
		 byte[] e = new byte[20 * _1MB];
		 byte[] f = new byte[10 * _1MB];
		 e = null;
		 f = null;
		 byte[] g = new byte[20 * _1MB];
 	}

}

debug並結合jvisualvm:

初始狀態,Eden被佔用11.201M:


執行 byte[] a = new byte[10 * _1MB]; byte[] b = new byte[10 * _1MB];byte[] c = new byte[15 * _1MB];創建c對象的時候觸發MinorGC,a,b對象到老年代,共20M,c(15M)到Eden:



執行 byte[] d = new byte[10 * _1MB]; byte[] e = new byte[20 * _1MB];創建e對象的時候再次觸發MinorGC,c,d到老年代,共25M,e(20M)到Eden:


執行 byte[] f = new byte[10 * _1MB];e = null; f = null; byte[] g = new byte[20 * _1MB];創建g對象的時候,當前新生代中對象有e和f,共30M,大於老年代剩餘空間5M,進而檢查是否允許擔保失敗,默認爲允許,繼續檢查之前2次晉升到老年代的對象平均值爲(20M+25M)/2=22.5M>5M ,將MinorGC改爲進行一次Full GC,下圖中可見Old Gen次數爲1,進行了一次Full GC:




擔保情景二:晉升到老年代的對象平均大小小於老年代剩餘空間


/**
 * 測試動態年齡判斷
 * -Xms100m -Xmx100m -Xmn50m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+HandlePromotionFailure=false -XX:PretenureSizeThreshold=30m -XX:+UseParNewGC -XX:+PrintFlagsFinal
 * 堆總大小100m,新生代總大小50m,Eden:S1:S2用的默認比例,8:1:1,Eden區40m,S1,S2各5m,避免大對象直接進老年代的干擾,把PretenureSizeThreshold配成30m
 * @author ljl
 */
public class TestEdenToAnther {
	private static int _1MB = 1*1024*1024;
	public static void main(String[] args) throws InterruptedException {
		 byte[] a = new byte[5 * _1MB];
		 byte[] b = new byte[5 * _1MB];
		 byte[] a_Survivor = new byte[2 * _1MB];//這裏要特別注意對象大小,不要下一次MinorGC被動態年齡判斷給回收到老年代去了
		 byte[] c = new byte[25 * _1MB];
		 c = null;
		 byte[] d = new byte[10 * _1MB];
		 byte[] e = new byte[20 * _1MB];
		 byte[] f = new byte[9 *  _1MB];
		 e = null;
		 f = null;
		 byte[] g = new byte[20 * _1MB];
 	}

}

debug並結合jvisualvm(此處不給出截圖了):

初始化狀態,Eden被佔用11.202M:


執行 byte[] a = new byte[5 * _1MB];  byte[] b = new byte[5 * _1MB];  byte[] a_Survivor = new byte[3 * _1MB];  byte[] c = new byte[25 * _1MB];創建c對象的時候觸發MinorGC ,

a,b到老年代,共10m,a_Survivor到s區,c(25m)存Eden:


執行 c = null; byte[] d = new byte[10 * _1MB];byte[] e = new byte[20 * _1MB];創建e對象的時候觸發MinorGC,d到老年代,共10m,e(20M)存Eden:


執行 byte[] f = new byte[9 *  _1MB]; e = null;  f = null;byte[] g = new byte[20 * _1MB];創建g對象的時候,當前年輕代對象有a_Survivor,e,f,總大小31M,大於老年代剩餘空間30M,進而檢查是否允許擔保失敗,默認爲允許,繼續檢查之前2次晉升到老年代的對象平均值爲(10M+10M)/2=10M<30M ,遂只進行MinorGC:



在擔保情景二下:晉升到老年代的對象平均大小小於老年代剩餘空間,設置參數HandlePromotionFailure=false,上面有說到因該參數在6.0_24 版本後被移除掉了,不接受配置,筆者的JDK版本爲1.7.0_79,理想測試結果爲執行到爲g分配空間時,仍然是觸發MinorGC,而不是FullGC,代碼就用情景二的代碼,測試結果如下:

控制檯會輸出一下信息,提示你該參數配置是無效的:


打印的GC日誌顯示只進行了MinorGC,因此,該參數確實是不接受配置的:




以上,便是JVM的整個內存分配及回收策略,筆者只是把教科書上的描述加以實驗,這樣自己也能加深印象,另外,在測試大對象直接分配到老年代的時候,遇到一個比較奇怪的問題,我提了個問題帖:http://bbs.csdn.net/topics/392176004,尚沒有解決,歡迎賜教或者討論。



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