Java技術(shù)體系中所提倡的自動(dòng)內(nèi)存管理最終可以歸結(jié)為自動(dòng)化地解決了兩個(gè)問(wèn)題:給對(duì)象分配內(nèi)存以及回收分配給對(duì)象的內(nèi)存禾进。
對(duì)象的內(nèi)存分配薇缅,往大方向上講,就是在堆上分配(但也可能經(jīng)過(guò)JIT編譯后被拆散為標(biāo)量類型并間接地在棧上分配)智厌,對(duì)象主要分配在新生代地Eden區(qū)上搜吧,如果啟動(dòng)了本地線程分配緩沖,將按線程優(yōu)先在TLAB上分配坦胶。少數(shù)情況下也可能會(huì)直接分配在老年代中透典,分配地規(guī)則并不是百分百固定地,其細(xì)節(jié)取決于當(dāng)前使用的是哪一種垃圾收集組合顿苇,還有虛擬機(jī)中與內(nèi)存的參數(shù)的設(shè)置峭咒。
對(duì)象優(yōu)先在EDen分配
大多數(shù)情況下,對(duì)象在新生代Eden區(qū)中分配纪岁。當(dāng)Eden區(qū)沒有足夠的空間進(jìn)行分配時(shí)凑队,虛擬機(jī)將發(fā)起一次Minor GC。
虛擬機(jī)提供了-XX:+PrintGCDetails這個(gè)收集器日志參數(shù)幔翰,告訴虛擬機(jī)在發(fā)生垃圾收集行為時(shí)打印內(nèi)存回收日志漩氨,并且在線程退出的時(shí)候輸出當(dāng)前內(nèi)存各區(qū)域的分配情況。在實(shí)際應(yīng)用中遗增,內(nèi)存回收日志一般是打印到這個(gè)文件后通過(guò)日志工具進(jìn)行分析才菠,不過(guò)本實(shí)驗(yàn)的日志并不多,直接閱讀就能看得很清楚贡定。
private static final int _1MB = 1024 * 1024;
//VM參數(shù):-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=0
public static void testAllocation(){
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];//出現(xiàn)一次Minor GC
}
運(yùn)行結(jié)果:
testAllocation()方法中,嘗試分配3個(gè)2MB大小和1個(gè)4MB大小的對(duì)象可都,在運(yùn)行時(shí)通過(guò)-Xms20M缓待、-Xmx20M和-Xmn10M這3個(gè)參數(shù)限制Java堆大小為20MB蚓耽,且不可擴(kuò)展,其中10MB分配給新生代旋炒,剩下的10MB分配給老年代步悠。-XX:SurvivorRatio=8決定了新生代中Eden區(qū)與一個(gè)Survivor區(qū)的空間比例是8比1,從輸出的結(jié)果也能清晰地看到”eden space 8192K瘫镇、from space 1025K鼎兽、to space 1024K“的信息,新生代總可用空間為9216KB(Eden區(qū)+1個(gè)Survivor區(qū)的總?cè)萘浚?/p>
執(zhí)行testAllocation()中分配allocation4對(duì)象的語(yǔ)句時(shí)會(huì)發(fā)生一次Minor GC铣除,這次GC的結(jié)果是新生代6651KB變?yōu)?48KB谚咬,而總內(nèi)存占用量則幾乎沒有減少(因?yàn)閍llocation1、2尚粘、3三個(gè)對(duì)象都是存活的择卦,虛擬機(jī)幾乎沒有找到可回收的對(duì)象)。這次GC發(fā)生的原因 是給allocation4分配內(nèi)存的時(shí)候郎嫁,發(fā)現(xiàn)Eden已經(jīng)被占用了6MB秉继,剩余空間已不足以分配allocation4所需的4MB內(nèi)存,因此發(fā)生Minor GC泽铛。GC期間虛擬機(jī)又發(fā)現(xiàn)已有的3個(gè)2MB大小的對(duì)象全部無(wú)法放入Survivor空間(Survivor空間只有1MB大猩屑),所以只好通過(guò)分配擔(dān)保機(jī)制提前轉(zhuǎn)移到老年代去盔腔。
這次GC結(jié)束后杠茬,4MB的allocation4對(duì)象被順利分配在Eden中。因此程序執(zhí)行完的結(jié)果是Eden占用4MB(被allocation4占用)铲觉,Survivor空閑澈蝙,老年代被占用6MB(被allocation1、2撵幽、3占用)灯荧,通過(guò)GC日志可以證實(shí)這一點(diǎn)。
注意 作者多次提到的Minor GC和Full GC有什么不一樣嗎盐杂?
- 新生代GC(Minor GC):指發(fā)生在新生代的垃圾手機(jī)動(dòng)作逗载,因?yàn)镴ava對(duì)象大多都具備朝生夕滅的特性,所以Minor GC非常頻繁链烈,一般回收速度也比較快厉斟。
- 老年代GC(Major GC/Full GC):指發(fā)生在老年代的GC,出現(xiàn)了Major GC强衡,經(jīng)常會(huì)伴隨至少一次的Minor GC(但非絕對(duì)的擦秽,在ParallelScavenge收集器的收集策略里就有直接進(jìn)行Major GC的策略選擇過(guò)程)。Major GC的速度一般會(huì)比Minor GC慢10倍以上。
大對(duì)象直接進(jìn)入老年代
所謂大對(duì)象就是指感挥,需要大量連續(xù)內(nèi)存空間的Java對(duì)象缩搅,最典型的大對(duì)象就是那種很長(zhǎng)的字符串及數(shù)組(byte[]數(shù)組就是典型的大對(duì)象)。大對(duì)象對(duì)虛擬機(jī)的內(nèi)存分配來(lái)說(shuō)就是一個(gè)壞消息(替Java虛擬機(jī)抱怨一句触幼,比遇到一個(gè)大對(duì)象更壞的消息就是遇到一群”朝生夕滅“的”短命大對(duì)象“硼瓣,寫程序的時(shí)候應(yīng)當(dāng)避免),經(jīng)常出現(xiàn)大對(duì)象容易導(dǎo)致內(nèi)存還有不少空間時(shí)就提前觸發(fā)垃圾收集以獲取足夠的連續(xù)空間來(lái)”安置“它們置谦。
虛擬機(jī)提供了一個(gè)-XX:PretenureSizeThreshold參數(shù)堂鲤,令大于這個(gè)設(shè)置值的對(duì)象直接在老年代中分配。這樣做的目的是避免在Eden區(qū)及兩個(gè)Survivor區(qū)之間發(fā)生大量的內(nèi)存拷貝媒峡。
private static final int _1MB = 1024 * 1024;
/**
* VM參數(shù):-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
* -XX:PretenureSizeThreshold=3145728
*/
public static void testPretenureSizeThreshold(){
byte[] allocation;
allocation = new byte[4 * _1MB];//直接分配在老年代中
}
運(yùn)行結(jié)果:
testPretenureSizeThreshold()方法后瘟栖,我們看到Eden空間幾乎沒有被使用,而老年代10MB的空間被使用了40%丝蹭,也就是4MB的allcoation對(duì)象直接就分配在老年代中慢宗,這是因?yàn)镻retenureSizeThreshold被設(shè)置為3MB(就是3145728B,這個(gè)參數(shù)不能與-Xmx之類的參數(shù)一樣直接寫3MB)奔穿,因此超過(guò)3MB的對(duì)象就會(huì)直接在老年代中進(jìn)行分配镜沽。
注意 PretenureSizeThreshold參數(shù)只對(duì)Serial和ParNew兩款收集器有效,Parallel Scavenge收集器不認(rèn)識(shí)這個(gè)參數(shù)贱田,Parallel Scavenge收集器一般并不需要設(shè)置缅茉。如果遇到必須使用此參數(shù)的場(chǎng)合,可以考慮ParNew加CMS的收集器組合
長(zhǎng)期存活的對(duì)象將進(jìn)入老年代
虛擬機(jī)既然采用了分代收集的思想來(lái)管理內(nèi)存男摧,那內(nèi)存回收時(shí)就必須能識(shí)別哪些對(duì)象應(yīng)當(dāng)放在新生代蔬墩,那些對(duì)象應(yīng)放在老年代中。為了做到這點(diǎn)耗拓,虛擬機(jī)給每個(gè)對(duì)象定義了一個(gè)對(duì)象年齡(Age)計(jì)數(shù)器拇颅。如果對(duì)象在Eden出生并經(jīng)過(guò)第一次Minor GC后仍然存活,并且能被Survivor容納的話乔询,將被移動(dòng)到Survivor空間中樟插,并將對(duì)象年齡設(shè)為1.對(duì)象在Survivor區(qū)中每熬過(guò)一次Minor GC,年齡就增加1歲竿刁,當(dāng)它的年齡增加到一定程度(默認(rèn)為15歲)時(shí)黄锤,就會(huì)被晉升到老年代中。對(duì)象晉升老年代的年齡閾值食拜,可以通過(guò)參數(shù)-XX:MaxTenuringThreshold來(lái)設(shè)置鸵熟。
動(dòng)態(tài)對(duì)象年齡判定
為了能更好地適應(yīng)不同程序的內(nèi)存狀況,虛擬機(jī)并不總是要求對(duì)象的年齡必須到達(dá)MaxTenuringThreshold才能晉升老年代负甸,如果在Survivor空間中相同年齡所有對(duì)象大小的總和大于Survivor空間的一半流强,年齡大于或等于該年齡的對(duì)象就可以直接進(jìn)入老年代痹届,無(wú)須等到MaxTenuringThreshold中要求的年齡。
private static final int _1MB = 1024 * 1024;
/**
* VM參數(shù):-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15
* -XX:+PrintTenuringDistribution
*/
@SuppressWarnings("unused")
public static void testTenuringThreshold2(){
byte[] allocation1,allocation2,allocation3,allocation4;
allocation1 = new byte[_1MB / 4];//allocation1+allocation2大于survivor空間的一半
allocation2 = new byte[_1MB / 4];
allocation3 = new byte[4 * _1MB];
allocation4 = new byte[4 * _1MB];
allocation4 = null;
allocation4 = new byte[4 * _1MB];
}
testTenuringThreshold2()方法打月,并設(shè)置參數(shù)-XX:MaxTenuringThreshold=15短纵,會(huì)發(fā)現(xiàn)運(yùn)行結(jié)果中Survivor的空間占用仍然為0%,而老年代比預(yù)期增加了6%僵控,也就是說(shuō)allocation1、allocation2對(duì)象都直接進(jìn)入了老年代鱼冀,而沒有等到15歲的臨界年齡报破。因?yàn)檫@兩個(gè)對(duì)象加起來(lái)已經(jīng)達(dá)到了512KB,并且它們是同年的千绪,滿足同年對(duì)象達(dá)到Survivor空間的一半規(guī)則充易。我們只要注釋掉其中一個(gè)對(duì)象的new操作,就會(huì)發(fā)現(xiàn)另外一個(gè)不會(huì)晉升到老年代中去了荸型。
空間分配擔(dān)保
在發(fā)生Minor GC時(shí)盹靴,虛擬機(jī)會(huì)檢測(cè)之前每次晉升到老年代的平均大小是否大于老年代的剩余空間大小,如果大于瑞妇,則改為直接進(jìn)行一次Full GC稿静。如果小于,則查看HandlePromotionFailure設(shè)置是否允許擔(dān)保失斣改备;如果允許,那只會(huì)進(jìn)行Minor GC蔓倍;如果不允許悬钳,則也要改為進(jìn)行一次Full GC。
前面提到過(guò)偶翅,新生代使用復(fù)制收集算法默勾,但為了內(nèi)存利用率,只是用其中一個(gè)Survivor空間來(lái)作為輪換備份聚谁,因此當(dāng)出現(xiàn)大量對(duì)象在Minor GC后仍然存活的情況時(shí)(最極端就是內(nèi)存回收后新生代中所有對(duì)象都存活)母剥,就需要老年代進(jìn)行分配擔(dān)保,讓Survivor無(wú)法容納的對(duì)象直接進(jìn)入老年代垦巴。與生活中的貸款擔(dān)保類似媳搪,老年代要進(jìn)行這樣的擔(dān)保,前提時(shí)老年代本身還有容納這些對(duì)象的剩余空間骤宣,一共有多少對(duì)象會(huì)活下來(lái)秦爆,在實(shí)際完成內(nèi)存回收之前是無(wú)法明確知道的,所以只好取之前每一次回收晉升到老年代對(duì)象容量的平均大小值作為經(jīng)驗(yàn)值憔披,與老年代的剩余空間進(jìn)行比較等限,決定是否進(jìn)行Full GC來(lái)讓老年代騰出更多空間爸吮。
取平均值進(jìn)行比較其實(shí)仍然是一種動(dòng)態(tài)概率的手段,也就是說(shuō)如果某次Minor GC存活后的對(duì)象突增望门,遠(yuǎn)遠(yuǎn)高于平均值的話形娇,依然會(huì)導(dǎo)致?lián)J。℉andle Promotion Failure)筹误。如果出現(xiàn)了Handle Promotion Failure失敗桐早,那就只好在失敗后重新發(fā)起一次Full GC。雖然擔(dān)保失敗時(shí)繞的圈子是最大的厨剪,但大部分情況下都還是會(huì)將Handle Promotion Failure開關(guān)打開哄酝,避免Full GC過(guò)于頻繁,參見如下代碼:
private static final int _1MB = 1024;
/**
* VM參數(shù):-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:-HandlePromotionFailure
*/
@SuppressWarnings("unused")
public static void testHandlePromotion(){
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];
}