1.Copying算法補充
目前主流虛擬機對新生代的對象回收都是用的這種算法匾效。IBM研究表明感论,新生代中的對象98%都是“朝生夕死”的乐横,所以并不需要按照1: 1來劃分內(nèi)存,而是將內(nèi)存分為一塊較大的Eden和兩塊較小的Survivor镇匀,每次使用使用Eden和一塊Survivor。當回收時闷袒,將Eden和Survivor中活著的對象全部復制到另一塊Survivor中坑律,然后清理掉Eden和原先的那一塊Survivor中全部的對象。HotSpot虛擬機默認的分配空間是8: 1,當然我們沒有辦法保證每次回收都只有不多于10%的對象存活晃择,當Survivor空間不夠用時冀值,需要依賴老年代進行分配擔保。
2. 內(nèi)存分配與回收策略
2.1 對象優(yōu)先在Eden分配
大多數(shù)情況下宫屠,對象在新生代Eden中分配列疗,當Eden區(qū)沒有空間時,虛擬機會發(fā)起一次MinorGC(在新生代中的垃圾回收動作)浪蹂。來看書中提供的這個例子
/**
* Created by cwj on 17-6-3.
* VM:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
*/
public class TestMinorGC {
public static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocatioon1,allocatioon2,allocatioon3,allocatioon4;
allocatioon1 = new byte[2 * _1MB];
allocatioon2 = new byte[2 * _1MB];
allocatioon3 = new byte[2 * _1MB];
allocatioon4 = new byte[4 * _1MB];
}
}
我使用的是idea+jdk1.8抵栈,所以與書中不同的是,要設置虛擬機使用Serial/Serial Old收集器坤次,-XX:+UseSerialGC古劲,如果不設置的話,idea會使用PS回收器缰猴,這樣效果就不一樣了产艾。看看執(zhí)行效果
代碼中滑绒,嘗試分配3個2M大小和1個4M大小的對象闷堡,運行時通過設置虛擬機參數(shù)將堆大小定位20M且不可擴展,其中新/老生代各占10M疑故。并且定義了Eden與Survivor比例是8:1杠览,從輸出結(jié)果也能看到:
eden space 8192K, from space 1024K, to space 1024K,其中新生代總共可用空間是9216KB(Eden+一個Survivor)纵势。
執(zhí)行到分配allocatioon4的空間是發(fā)生了一次MinorGC踱阿,結(jié)果是新生代從7581kb變到336kb,而總內(nèi)存是7518kb變到6480kb吨悍,幾乎沒怎么變扫茅,因為1,2,3這三個對象都是存活的,并沒有被回收育瓜。這次GC發(fā)生的原因是給4分配內(nèi)存的時候,發(fā)現(xiàn)Eden已經(jīng)被占用了6M葫隙,剩余的空間不足以分配4所需要的4M空間,所以發(fā)生了MinorGC躏仇。期間虛擬機又發(fā)現(xiàn)無法將3個2M大小的對象全部放入Survivor空間(因為Survivor只有1M)恋脚,所以只能通過擔保機制,將3個對象轉(zhuǎn)移到老年代中焰手。
這次GC結(jié)束后糟描,4M的allocation4對象被順利的分配到了Eden中,因此程序執(zhí)行結(jié)果是Eden占用了4M书妻,Survivor空閑船响,老年代被占用了6M。
2.2 大對象直接進入老年代
所謂的大對象是指需要連續(xù)大量內(nèi)存空間的Java對象,最典型的大對象就是那種很長的字符串和數(shù)組见间。經(jīng)常出現(xiàn)大對象容易導致內(nèi)存還有不少空間時就提前出發(fā)垃圾回收器以獲得足夠多的連續(xù)空間來安置他們
虛擬機提供了一個 -XX:PretenureSizeThreshold 參數(shù)聊闯,大于這個參數(shù)的對象直接進入老年代。這樣做的目的是為了避免Eden和兩個Survivor之間發(fā)生大量的內(nèi)存復制米诉。
/**
* Created by cwj on 17-6-3.
* VM:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
* -XX:PretenureSizeThreshold=3145728
*
*/
public class TestMinorGC {
public static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocatioon;
allocatioon = new byte[4 * _1MB];
}
}
結(jié)果很清楚菱蔬,因為我們預先設置了參數(shù)-XX:PretenureSizeThreshold=3145728,所以大于3M的對象直接就被分到了老年代中史侣,所以老年代占了4M拴泌,新生代空閑。
2.3 長期存活的對象將進入老年代
虛擬機采用了分代收集的思想來管理內(nèi)存惊橱,那么內(nèi)存回+收時就必須知道哪些對象應該放在新生代蚪腐,哪些放在老年代。為了做到這點税朴,虛擬機給每個對象定義了一個年齡計數(shù)器削茁。如果對象在Eden出生并經(jīng)過一次GC后仍然存活,并且能被Survivor容納的話掉房,將被移動到Survivor空間中,并且對象年齡設置為1慰丛。對象在Survivor中每熬過一次GC卓囚,年齡就增大一歲,當它的年齡增大到一定歲數(shù)時(默認15歲)诅病,就會被移到老年代哪亿。當然這個默認值可以通過修改參數(shù) -XX:MaxTenuringThreshold的值來改變。
/**
* Created by cwj on 17-6-3.
*
* VM:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
* -XX:MaxTenuringThreshold=1
* -XX:+PrintTenuringDistribution
*/
public class TestTenuringThreshold {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation1,allocation2,allocation3;
allocation1 = new byte[_1MB / 4];
allocation2 = new byte[4 * _1MB];
allocation3 = new byte[4 * _1MB];
//第一次GC
allocation3 = null;
allocation3 = new byte[4 * _1MB];
//第二次GC
}
}
我們分析一下:首先1,2都被分到Eden中贤笆,當3第一次來時蝇棉,Eden不夠4M了,所以進行一次GC芥永,將1放入Survivor篡殷,Age+1。又因為Survivor只有1M埋涧,不夠4M板辽,所以直接擔保機制,2放入老年代棘催。所以Eden從5726k->616k劲弦,總內(nèi)存基本沒變。
當?shù)诙?進來時醇坝,發(fā)生第二次GC邑跪,將原來存在Eden中的3釋放(賦值為null了),Survivor中的1已經(jīng)1歲了,所以存入老年代,這時Eden剛釋放完3画畅,下一個3還沒有進來砸琅,所以Eden從4712k->0,總內(nèi)存由8808k->4706k,GC過后將新的3存入Eden夜赵。兩次GC過后明棍。Eden中存著的是3,老年代中存著的是2和1寇僧。
如果將參數(shù) -XX:MaxTenuringThreshold值改為15摊腋,兩次GC過后應該是Eden中存著3,Survivor中存著1嘁傀,老年代中存著2.(理論是這樣兴蒸,然而我的idea中跑的結(jié)果還是跟參數(shù)為1的情況一樣,很納悶细办。橙凳。。)
2.4 動態(tài)對象年齡判定
為了能更好的適應不同程序的內(nèi)存情況笑撞,虛擬機并不是永遠要求對象的年齡必須達到MaxTenuringThreshold參數(shù)的值時才能晉升到老年代岛啸。如果在Survivor中所有相同年齡的對象大小的總和超過Survivor空間的一半,則年齡大于或等于該年齡對象就可以直接進入老年代茴肥,無需等到參數(shù)設定的年齡
/**
* Created by cwj on 17-6-3.
*
* VM:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
* -XX:MaxTenuringThreshold=15
* -XX:+PrintTenuringDistribution
*/
public class TestTenuringThreshold2 {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
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];
//第一次GC
allocation4 = null;
allocation4 = new byte[4 * _1MB];
//第二次GC
}
}
可以看到結(jié)果中Survivor仍然是0坚踩,而老年代比預計的增加了6%,說明1,2并沒有等到15歲就直接進入了老年代瓤狐,因為這兩個對象加起來的和已經(jīng)達到了Survivor空間的一半瞬铸,并且他們是同年的。我們只要注釋掉一個础锐,另外一個就不會進入老年代了嗓节。
2.5 空間分配擔保
新生代不夠放了就往老年代里面挪,那老年代也有不夠的時候啊皆警,這時候怎么辦呢拦宣?如果老年代的連續(xù)空間大于新生代對象的總大小,或者歷次晉升的平均大小信姓,就可以進行安全的MinorGC恢着,如果不大于那么就會執(zhí)行一次FullGC來釋放老年代的空間。
/**
* Created by cwj on 17-6-3.
*
* VM:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
* -XX:+PrintTenuringDistribution
*/
public class TestTenuringThreshold3 {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
byte[] allocation1,allocation2,allocation3,allocation4,allocation5,allocation6,allocation7;
allocation1 = new byte[2 * _1MB];
allocation2 = new byte[2 * _1MB];
allocation3 = new byte[2 * _1MB];
allocation1 = null;
allocation4 = new byte[2 * _1MB];
allocation5 = new byte[2 * _1MB];
allocation6 = new byte[2 * _1MB];
allocation4 = null;
allocation5 = null;
allocation6 = null;
allocation7 = new byte[2 * _1MB];
}
}