JVM學(xué)習(xí)(5) 內(nèi)存分配與回收策略

Java技術(shù)體系中所提倡的自動內(nèi)存管理最終可以歸結(jié)為自動化地解決了兩個(gè)問題:給對象分配內(nèi)存以及回收分配給對象的內(nèi)存。

對象的內(nèi)存分配,往大方向上講,就是在堆上分配(但也可能經(jīng)過JIT編譯后被拆散為標(biāo)量類型并間接地在棧上分配),對象主要分配在新生代地Eden區(qū)上,如果啟動了本地線程分配緩沖,將按線程優(yōu)先在TLAB上分配。少數(shù)情況下也可能會直接分配在老年代中,分配地規(guī)則并不是百分百固定地,其細(xì)節(jié)取決于當(dāng)前使用的是哪一種垃圾收集組合,還有虛擬機(jī)中與內(nèi)存的參數(shù)的設(shè)置。

對象優(yōu)先在EDen分配

大多數(shù)情況下,對象在新生代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è)文件后通過日志工具進(jìn)行分析,不過本實(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é)果:

162.png

testAllocation()方法中,嘗試分配3個(gè)2MB大小和1個(gè)4MB大小的對象,在運(yùn)行時(shí)通過-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對象的語句時(shí)會發(fā)生一次Minor GC,這次GC的結(jié)果是新生代6651KB變?yōu)?48KB,而總內(nèi)存占用量則幾乎沒有減少(因?yàn)閍llocation1、2、3三個(gè)對象都是存活的,虛擬機(jī)幾乎沒有找到可回收的對象)。這次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大小的對象全部無法放入Survivor空間(Survivor空間只有1MB大?。灾缓猛ㄟ^分配擔(dān)保機(jī)制提前轉(zhuǎn)移到老年代去。

這次GC結(jié)束后,4MB的allocation4對象被順利分配在Eden中。因此程序執(zhí)行完的結(jié)果是Eden占用4MB(被allocation4占用),Survivor空閑,老年代被占用6MB(被allocation1、2、3占用),通過GC日志可以證實(shí)這一點(diǎn)。


注意 作者多次提到的Minor GC和Full GC有什么不一樣嗎?

  • 新生代GC(Minor GC):指發(fā)生在新生代的垃圾手機(jī)動作,因?yàn)镴ava對象大多都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快。
  • 老年代GC(Major GC/Full GC):指發(fā)生在老年代的GC,出現(xiàn)了Major GC,經(jīng)常會伴隨至少一次的Minor GC(但非絕對的,在ParallelScavenge收集器的收集策略里就有直接進(jìn)行Major GC的策略選擇過程)。Major GC的速度一般會比Minor GC慢10倍以上。

大對象直接進(jìn)入老年代

所謂大對象就是指,需要大量連續(xù)內(nèi)存空間的Java對象,最典型的大對象就是那種很長的字符串及數(shù)組(byte[]數(shù)組就是典型的大對象)。大對象對虛擬機(jī)的內(nèi)存分配來說就是一個(gè)壞消息(替Java虛擬機(jī)抱怨一句,比遇到一個(gè)大對象更壞的消息就是遇到一群”朝生夕滅“的”短命大對象“,寫程序的時(shí)候應(yīng)當(dāng)避免),經(jīng)常出現(xiàn)大對象容易導(dǎo)致內(nèi)存還有不少空間時(shí)就提前觸發(fā)垃圾收集以獲取足夠的連續(xù)空間來”安置“它們。

虛擬機(jī)提供了一個(gè)-XX:PretenureSizeThreshold參數(shù),令大于這個(gè)設(shè)置值的對象直接在老年代中分配。這樣做的目的是避免在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é)果:

163.png

testPretenureSizeThreshold()方法后,我們看到Eden空間幾乎沒有被使用,而老年代10MB的空間被使用了40%,也就是4MB的allcoation對象直接就分配在老年代中,這是因?yàn)镻retenureSizeThreshold被設(shè)置為3MB(就是3145728B,這個(gè)參數(shù)不能與-Xmx之類的參數(shù)一樣直接寫3MB),因此超過3MB的對象就會直接在老年代中進(jìn)行分配。


注意 PretenureSizeThreshold參數(shù)只對Serial和ParNew兩款收集器有效,Parallel Scavenge收集器不認(rèn)識這個(gè)參數(shù),Parallel Scavenge收集器一般并不需要設(shè)置。如果遇到必須使用此參數(shù)的場合,可以考慮ParNew加CMS的收集器組合


長期存活的對象將進(jìn)入老年代

虛擬機(jī)既然采用了分代收集的思想來管理內(nèi)存,那內(nèi)存回收時(shí)就必須能識別哪些對象應(yīng)當(dāng)放在新生代,那些對象應(yīng)放在老年代中。為了做到這點(diǎn),虛擬機(jī)給每個(gè)對象定義了一個(gè)對象年齡(Age)計(jì)數(shù)器。如果對象在Eden出生并經(jīng)過第一次Minor GC后仍然存活,并且能被Survivor容納的話,將被移動到Survivor空間中,并將對象年齡設(shè)為1.對象在Survivor區(qū)中每熬過一次Minor GC,年齡就增加1歲,當(dāng)它的年齡增加到一定程度(默認(rèn)為15歲)時(shí),就會被晉升到老年代中。對象晉升老年代的年齡閾值,可以通過參數(shù)-XX:MaxTenuringThreshold來設(shè)置。

動態(tài)對象年齡判定

為了能更好地適應(yīng)不同程序的內(nèi)存狀況,虛擬機(jī)并不總是要求對象的年齡必須到達(dá)MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進(jìn)入老年代,無須等到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,會發(fā)現(xiàn)運(yùn)行結(jié)果中Survivor的空間占用仍然為0%,而老年代比預(yù)期增加了6%,也就是說allocation1、allocation2對象都直接進(jìn)入了老年代,而沒有等到15歲的臨界年齡。因?yàn)檫@兩個(gè)對象加起來已經(jīng)達(dá)到了512KB,并且它們是同年的,滿足同年對象達(dá)到Survivor空間的一半規(guī)則。我們只要注釋掉其中一個(gè)對象的new操作,就會發(fā)現(xiàn)另外一個(gè)不會晉升到老年代中去了。

空間分配擔(dān)保

在發(fā)生Minor GC時(shí),虛擬機(jī)會檢測之前每次晉升到老年代的平均大小是否大于老年代的剩余空間大小,如果大于,則改為直接進(jìn)行一次Full GC。如果小于,則查看HandlePromotionFailure設(shè)置是否允許擔(dān)保失敗;如果允許,那只會進(jìn)行Minor GC;如果不允許,則也要改為進(jìn)行一次Full GC。

前面提到過,新生代使用復(fù)制收集算法,但為了內(nèi)存利用率,只是用其中一個(gè)Survivor空間來作為輪換備份,因此當(dāng)出現(xiàn)大量對象在Minor GC后仍然存活的情況時(shí)(最極端就是內(nèi)存回收后新生代中所有對象都存活),就需要老年代進(jìn)行分配擔(dān)保,讓Survivor無法容納的對象直接進(jìn)入老年代。與生活中的貸款擔(dān)保類似,老年代要進(jìn)行這樣的擔(dān)保,前提時(shí)老年代本身還有容納這些對象的剩余空間,一共有多少對象會活下來,在實(shí)際完成內(nèi)存回收之前是無法明確知道的,所以只好取之前每一次回收晉升到老年代對象容量的平均大小值作為經(jīng)驗(yàn)值,與老年代的剩余空間進(jìn)行比較,決定是否進(jìn)行Full GC來讓老年代騰出更多空間。

取平均值進(jìn)行比較其實(shí)仍然是一種動態(tài)概率的手段,也就是說如果某次Minor GC存活后的對象突增,遠(yuǎn)遠(yuǎn)高于平均值的話,依然會導(dǎo)致?lián)J。℉andle Promotion Failure)。如果出現(xiàn)了Handle Promotion Failure失敗,那就只好在失敗后重新發(fā)起一次Full GC。雖然擔(dān)保失敗時(shí)繞的圈子是最大的,但大部分情況下都還是會將Handle Promotion Failure開關(guān)打開,避免Full GC過于頻繁,參見如下代碼:

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];
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容