六、內(nèi)存分配與回收策略
Java
技術(shù)體系中所倡導(dǎo)的自動(dòng)內(nèi)存管理最終可以歸納為自動(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)存相關(guān)的參數(shù)的設(shè)置。
6.1 對(duì)象優(yōu)先在Eden分配
大多數(shù)情況下,對(duì)象在新生代Eden
區(qū)中分配。當(dāng)Eden
區(qū)沒(méi)有足夠空間進(jìn)行分配時(shí),虛擬機(jī)將發(fā)起一次Minor GC
。虛擬機(jī)提供了-XX:+PrintGCDetails
這個(gè)收集器日志參數(shù),告訴虛擬機(jī)在發(fā)生垃圾收集行為時(shí)打印內(nèi)存回收日志,并且在進(jìn)程退出的時(shí)候輸出當(dāng)前的內(nèi)存各區(qū)域分配情況。
private static fianl int _1MB = 1024 * 1024;
/*
VM參數(shù):-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRation=8
*/
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é)果:
說(shuō)明:
代碼中可以看到,嘗試分配三個(gè)
2MB
大小和一個(gè)4MB
大小的對(duì)象,在運(yùn)行時(shí)通過(guò)-Xms20M、-Xmx20M、-Xmn10M
這三個(gè)參數(shù)限制了Java
堆大小為20MB
,不可擴(kuò)展,其中10MB
分配給老年代。-XX:SurvivorRation=8
決定了新生代中Eden
區(qū)與一個(gè)Survivor
區(qū)的空間比例是8:1
,于是新生代總的可用空間是9216KB
。-
從運(yùn)行結(jié)果可以看到,在分配
allocation4
對(duì)象的語(yǔ)句時(shí)會(huì)發(fā)生一次Minor GC
,從結(jié)果第一行可以看到,新生代占用空間從6651-->148
,但是需要知道,在發(fā)生GC
的時(shí)候,首先要將Eden
空間中的對(duì)象向一個(gè)Survivor
區(qū)中轉(zhuǎn)移,但是這里Survivor
空間只有1MB
,那只好轉(zhuǎn)移到老年代中去了,所以,看以看到發(fā)生GC
的時(shí)候堆的總占用量并沒(méi)有減少(6651K-->6292K(19456K)
可以看出,因?yàn)橹暗娜齻€(gè)對(duì)象并沒(méi)有死亡),而老年代被占用60%
(從the space 10240K, 60%
可以看出,3
個(gè)對(duì)象總共占用6M
,老年代有10M
),之后分配的第四個(gè)對(duì)象占用4M
,而Eden
為8M
,所以占用50%
左右(從eden space 8192, 51%
看出),這里form space
占用14%
可能是一些其他信息吧(不太確定),而to space
因?yàn)榇婊顚?duì)象都轉(zhuǎn)移到老年代中去了,所以這里沒(méi)有占用。這里借用一張圖(http://www.lxweimin.com/p/fab8865f9b79
)
2
Minor GC 和 Full GC 的區(qū)別
- 新生代
GC
(Minor GC
):指發(fā)生在新生代的垃圾收集動(dòng)作,因?yàn)?code>Java對(duì)象大多都是具備朝生夕滅的特性,所有Minor GC
非常頻繁,一般回收速度也比較快。 - 老年代
GC
(Major GC/ Full GC
):指發(fā)生在老年代的GC
,出現(xiàn)了Major GC
,經(jīng)常會(huì)伴隨至少一次的Minor GC
(但非絕對(duì)的,在Parallel Scavenge
收集器的收集策略里就有直接進(jìn)行Major GC
的策略選擇過(guò)程)。Major GC
的速度一般會(huì)比Minor GC
慢十倍以上。
6.2 大對(duì)象直接進(jìn)入老年代
所謂的大對(duì)象是指,需要大量連續(xù)內(nèi)存空間的Java
對(duì)象,最典型的大對(duì)象就是那種很長(zhǎng)的字符串或數(shù)組,之前例子中就是數(shù)組大對(duì)象。大對(duì)象在內(nèi)存中有時(shí)候不太好處理,會(huì)導(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)存復(fù)制。下面通過(guò)例子說(shuō)明:
說(shuō)明:從運(yùn)行結(jié)果來(lái)看,新生代根本就沒(méi)有使用,因?yàn)槲覀冊(cè)O(shè)置的
XX:PretenureSizeThreshold
參數(shù)值為3145728bit
(即3MB
),而分配的對(duì)象為4MB
,所以直接在老年代中分配,于是老年代被占用40%
。注意:
XX:PretenureSizeThreshold
參數(shù)只對(duì)Serial
和ParNew
有效,Parallel Scavenge
不認(rèn)識(shí)此參數(shù),Parallel Scavenge
一般并不需要設(shè)置。如果遇到必須使用此參數(shù)的場(chǎng)合,可以考慮ParNew
加CMS
的收集器組合。
6.3 長(zhǎng)期存活的對(duì)象將進(jìn)入老年代
既然虛擬機(jī)采用了分代收集的思想來(lái)管理內(nèi)存,那么內(nèi)存回收時(shí)就必須能識(shí)別哪些對(duì)象應(yīng)放在新生代,哪些對(duì)象應(yīng)放在老年代中。這里,虛擬機(jī)中使用對(duì)象年齡(Age
)計(jì)數(shù)器來(lái)完成此目的。如果對(duì)象在Eden
出生并經(jīng)過(guò)一次Minor GC
后仍然存活,并且能被Survivor
容納的話,將被移動(dòng)到Survivor
空間中,并且對(duì)象年齡設(shè)為1
。對(duì)象在Survivor
區(qū)中沒(méi)“熬過(guò)”一次Minor GC
,年齡就增加一歲,當(dāng)它的年齡增加到一定程度(默認(rèn)為15
歲),就將會(huì)被晉升到老年代中。對(duì)象晉升老年代的閾值可以通過(guò)參數(shù)-XX:MaxTenuringThreshold
設(shè)置。
下面對(duì)同一個(gè)代碼設(shè)置不同的-XX:MaxTenuringThreshold
值來(lái)進(jìn)行說(shuō)明:
-
程序代碼:
4 -
以
MaxTenuringThreshold=1
參數(shù)來(lái)運(yùn)行的結(jié)果:
5
6 -
以
MaxTenuringThreshold=15
參數(shù)來(lái)運(yùn)行的結(jié)果:
7
說(shuō)明:對(duì)象allocation1
需要256K
的內(nèi)存,Survivor
空間能夠容納。當(dāng)MaxTenuringThreshold=1
時(shí),allocation1
對(duì)象在第二次GC
發(fā)生時(shí)進(jìn)入老年代,新生代已使用的內(nèi)存GC
后非常干凈地變成了0KB
。而MaxTenuringThreshold=15
時(shí),第二次發(fā)生GC
后,allocation1
對(duì)象則還留在新生代Survivor
空間,這時(shí)新生代仍然有404KB
被占用。(這里沒(méi)有完全弄懂)
6.4 動(dòng)態(tài)對(duì)象年齡判定
為了能更好地適應(yīng)不同程序的內(nèi)存狀況,虛擬機(jī)并不是永遠(yuǎn)地要求對(duì)象的年齡必須達(dá)到MaxTenuringThreshold
才能晉升老年代,如果在Survivor
空間中相同年齡所有對(duì)象大小的總和大于Survivor
空間的一半,年齡大于或等于該年齡的對(duì)象就可以直接進(jìn)入老年代,無(wú)須等到MaxTenuringThreshold
中要求的年齡。下面通過(guò)代碼說(shuō)明:
說(shuō)明:這里設(shè)置
MaxTenuringThreshold=15
,會(huì)發(fā)現(xiàn)運(yùn)行結(jié)果中Survivor
的空間占用仍然為0
,而老年代比預(yù)期增加了6%
,即allocation1、allocation2
對(duì)象都直接進(jìn)入了老年代,而沒(méi)有等到15
歲的臨界值。因?yàn)檫@兩個(gè)對(duì)象加起來(lái)已經(jīng)達(dá)到512KB
,并且它們是同年的,滿足了一半的規(guī)則。我們只需要注釋掉其中一個(gè)對(duì)象new
操作,就會(huì)發(fā)現(xiàn)另外一個(gè)就不會(huì)晉升到老年代中了。
6.5 空間分配擔(dān)保
在發(fā)生Minor GC
之前,虛擬機(jī)會(huì)先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對(duì)象總空間,如果這個(gè)條件成立,那么Minor GC
可以確保是安全的。如果不成立,則虛擬機(jī)會(huì)查看HandlePromotionFailure
設(shè)置值是否允許擔(dān)保失敗。如果允許,那么會(huì)繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對(duì)象的平均大小,如果大于,將嘗試著進(jìn)行一次Minor GC
,盡管這次Minor GC
是有風(fēng)險(xiǎn)的;如果小于,或者HandlePromotionFailure
設(shè)置不允許冒險(xiǎn),那這是也要該為進(jìn)行一次Full GC
。
什么是“冒險(xiǎn)”,前面提過(guò),新生代使用復(fù)制收集算法,但為了內(nèi)存利用率,只使用其中一個(gè)Survivor
空間來(lái)作為輪換備份,因此當(dāng)出現(xiàn)大量對(duì)象在Minor GC
后仍然存活的情況(最極端的情況就是內(nèi)存回收后新生代中所有對(duì)象都存活),就需要老年代進(jìn)行分配擔(dān)保,把Survivor
無(wú)法容納的對(duì)象直接進(jìn)入老年代。但是老年代本身不知道是否還有容納這些對(duì)象的剩余空間,所以只好取之前每一次回收晉升到老年代對(duì)象容量的平均大小值作為經(jīng)驗(yàn)值,與老年代的剩余空間進(jìn)行比較,決定是否進(jìn)行Full GC
來(lái)讓老年代騰出更多空間。
下面通過(guò)代碼說(shuō)明:
說(shuō)明:取平均值進(jìn)行比較其實(shí)仍然是一種動(dòng)態(tài)概率的手段,如果某次
Minor GC
存活胡的對(duì)象突增,遠(yuǎn)遠(yuǎn)高于平均值的話,仍然會(huì)導(dǎo)致?lián)J。?code>Handle Promotion Failure)。如果出現(xiàn)失敗,那就只好再次發(fā)起一次Full GC
。雖然擔(dān)保失敗時(shí)繞的圈子是最大的,但大部分情況下都還是會(huì)將HandlePromotionFailure
開(kāi)關(guān)打開(kāi),避免Full GC
過(guò)于頻繁。
上述內(nèi)容感覺(jué)書(shū)上講的不是很詳細(xì),有些東西還沒(méi)搞懂,在以后補(bǔ)充!!!