Java技術(shù)體系中所提倡的自動(dòng)內(nèi)存管理最終可以歸結(jié)為自動(dòng)化地解決了兩個(gè)問(wèn)題:給對(duì)象分配內(nèi)存以及回收分配給對(duì)象的內(nèi)存。
對(duì)象主要分配在堆上的Eden,如果啟用的TLAB,那優(yōu)先在TLAB上分配,少數(shù)情況會(huì)直接分配到老年代中,分配的規(guī)則不會(huì)100%確定的,取決于使用什么垃圾收集器,還有虛擬機(jī)相關(guān)參數(shù)設(shè)置
接下來(lái)我們?cè)赟erial/Serial Old收集器下驗(yàn)證幾種規(guī)則
1. 對(duì)象優(yōu)先在Eden中分配

執(zhí)行
javac Test.java
java -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC Test
結(jié)果

解析:
-Xms20M、-Xmx20M、-Xmn10M這3個(gè)參數(shù)限制了Java堆大小為20MB,不可擴(kuò)展,其中10MB分配給新生代,剩下的10MB分配給老年代。-XX:SurvivorRatio=8決定了新生代中Eden區(qū)與一個(gè)Survivor區(qū)的空間比例是8:1,從輸出的結(jié)果也可以清晰地看到"edenspace8192K、fromspace1024K、tospace1024K"的信息,新生代總可用空間為9216KB(Eden區(qū)+1個(gè)Survivor區(qū)的總?cè)萘浚?strong>-XX:UseSerialGC 指定使用Serial收集器。
執(zhí)行testAllocation()中分配allocation4對(duì)象的語(yǔ)句時(shí)會(huì)發(fā)生一次MinorGC,這次GC的結(jié)果是新生代6816KB變?yōu)?79KB,而總內(nèi)存占用量則幾乎沒(méi)有減少(因?yàn)閍llocation1、allocation2、allocation3三個(gè)對(duì)象都是存活的,虛擬機(jī)幾乎沒(méi)有找到可回收的對(duì)象)。這次GC發(fā)生的原因是給allocation4分配內(nèi)存的時(shí)候,發(fā)現(xiàn)Eden已經(jīng)被占用了6MB,剩余空間已不足以分配allocation4所需的4MB內(nèi)存,因此發(fā)生MinorGC。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、allocation2、allocation3占用)。通過(guò)GC日志可以證實(shí)這一點(diǎn)
2. 大對(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ì)象”,寫(xiě)程序的時(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)存復(fù)制

javac Test.javajava -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC Test
java -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:PretenureSizeThreshold=3145728 Test
第一次直接分配在了eden空間,第二次指定-XX:PretenureSizeThreshold參數(shù)為3MB(就是3145728,這個(gè)參數(shù)不能像-Xmx之類的參數(shù)一樣直接寫(xiě)3MB),因此超過(guò)3MB的對(duì)象都會(huì)直接在老年代進(jìn)行分配,所以第二次老年代內(nèi)存占比40%。
注意PretenureSizeThreshold參數(shù)只對(duì)Serial和ParNew兩款收集器有效,ParallelScavenge收集器不認(rèn)識(shí)這個(gè)參數(shù),ParallelScavenge收集器一般并不需要設(shè)置。如果遇到必須使用此參數(shù)的場(chǎng)合,可以考慮ParNew加CMS的收集器組合。
3. 長(zhǎng)期存活的對(duì)象進(jìn)入老年代
既然虛擬機(jī)采用了分代收集的思想來(lái)管理內(nèi)存,那么內(nèi)存回收時(shí)就必須能識(shí)別哪些對(duì)象應(yīng)放在新生代,哪些對(duì)象應(yīng)放在老年代中。為了做到這點(diǎn),虛擬機(jī)給每個(gè)對(duì)象定義了一個(gè)對(duì)象年齡(Age)計(jì)數(shù)器。如果對(duì)象在Eden出生并經(jīng)過(guò)第一次MinorGC后仍然存活,并且能被Survivor容納的話,將被移動(dòng)到Survivor空間中,并且對(duì)象年齡設(shè)為1。對(duì)象在Survivor區(qū)中每“熬過(guò)”一次MinorGC,年齡就增加1歲,當(dāng)它的年齡增加到一定程度(默認(rèn)為15歲),就將會(huì)被晉升到老年代中。對(duì)象晉升老年代的年齡閾值,可以通過(guò)參數(shù)-XX:MaxTenuringThreshold設(shè)置。
讀者可以試試分別以-XX:MaxTenuringThreshold=1和-XX:MaxTenuringThreshold=15兩種設(shè)置來(lái)執(zhí)行代碼

javac Test.java
java -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:MaxTenuringThreshold=1 Test

java -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:MaxTenuringThreshold=15 Test
此方法中的allocation1對(duì)象需要(1024/5)KB內(nèi)存,Survivor空間可以容納。當(dāng)MaxTenuringThreshold=1時(shí),allocation1對(duì)象在第二次GC發(fā)生時(shí)進(jìn)入老年代,新生代已使用的內(nèi)存GC后非常干凈地變成0KB。而MaxTenuringThreshold=15時(shí),第二次GC發(fā)生后,allocation1對(duì)象則還留在新生代Survivor空間,這時(shí)新生代仍然有482KB被占用。
4. 動(dòng)態(tài)年齡判定
為了能更好地適應(yīng)不同程序的內(nèi)存狀況,虛擬機(jī)并不是永遠(yuǎn)地要求對(duì)象的年齡必須達(dá)到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對(duì)象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對(duì)象就可以直接進(jìn)入老年代,無(wú)須等到MaxTenuringThreshold中要求的年齡。
5. 空間分配擔(dān)保
- JDK6Update24之前:
下面解釋一下“冒險(xiǎn)”是冒了什么風(fēng)險(xiǎn),前面提到過(guò),新生代使用復(fù)制收集算法,但為了內(nèi)存利用率,只使用其中一個(gè)Survivor空間來(lái)作為輪換備份,因此當(dāng)出現(xiàn)大量對(duì)象在MinorGC后仍然存活的情況(最極端的情況就是內(nèi)存回收后新生代中所有對(duì)象都存活),就需要老年代進(jìn)行分配擔(dān)保,把Survivor無(wú)法容納的對(duì)象直接進(jìn)入老年代。與生活中的貸款擔(dān)保類似,老年代要進(jìn)行這樣的擔(dān)保,前提是老年代本身還有容納這些對(duì)象的剩余空間,一共有多少對(duì)象會(huì)活下來(lái)在實(shí)際完成內(nèi)存回收之前是無(wú)法明確知道的,所以只好取之前每一次回收晉升到老年代對(duì)象容量的平均大小值作為經(jīng)驗(yàn)值,與老年代的剩余空間進(jìn)行比較,決定是否進(jìn)行FullGC來(lái)讓老年代騰出更多空間。取平均值進(jìn)行比較其實(shí)仍然是一種動(dòng)態(tài)概率的手段,也就是說(shuō),如果某次MinorGC存活后的對(duì)象突增,遠(yuǎn)遠(yuǎn)高于平均值的話,依然會(huì)導(dǎo)致?lián)J。℉andlePromotionFailure)。如果出現(xiàn)了HandlePromotionFailure失敗,那就只好在失敗后重新發(fā)起一次FullGC。雖然擔(dān)保失敗時(shí)繞的圈子是最大的,但大部分情況下都還是會(huì)將HandlePromotionFailure開(kāi)關(guān)打開(kāi),避免FullGC過(guò)于頻繁。 -
在JDK6Update24之后:
HandlePromotionFailure參數(shù)不會(huì)再影響到虛擬機(jī)的空間分配擔(dān)保策略,雖然源碼中還定義了HandlePromotionFailure參數(shù),但是在代碼中已經(jīng)不會(huì)再使用它。JDK6Update24之后的規(guī)則變?yōu)橹灰夏甏倪B續(xù)空間大于新生代對(duì)象總大小或者歷次晉升的平均大小就會(huì)進(jìn)行MinorGC,否則將進(jìn)行FullGC。
6. 小節(jié)
內(nèi)存回收與垃圾收集器在很多時(shí)候都是影響系統(tǒng)性能、并發(fā)能力的主要因素之一,虛擬機(jī)之所以提供多種不同的收集器以及提供大量的調(diào)節(jié)參數(shù),是因?yàn)橹挥懈鶕?jù)實(shí)際應(yīng)用需求、實(shí)現(xiàn)方式選擇最優(yōu)的收集方式才能獲取最高的性能。沒(méi)有固定收集器、參數(shù)組合,也沒(méi)有最優(yōu)的調(diào)優(yōu)方法,虛擬機(jī)也就沒(méi)有什么必然的內(nèi)存回收行為。
日志里GC和FullGC概念:
GC日志開(kāi)頭的"[GC"和"[FullGC"說(shuō)明了這次垃圾收集的停頓類型,而不是用來(lái)區(qū)分新生代GC還是老年代GC的。如果有"Full",說(shuō)明這次GC是發(fā)生了Stop-The-World的,新生代收集器ParNew的日志也會(huì)出現(xiàn)"[FullGC"(這一般是因?yàn)槌霈F(xiàn)了分配擔(dān)保失敗之類的問(wèn)題,所以才導(dǎo)致STW)。如果是調(diào)用System.gc()方法所觸發(fā)的收集,那么在這里將顯示"[FullGC(System)"。
多次提到的MinorGC和FullGC有什么不一樣嗎?:
- 新生代GC(MinorGC):指發(fā)生在新生代的垃圾收集動(dòng)作,因?yàn)镴ava對(duì)象大多都具備朝生夕滅的特性,所以MinorGC非常頻繁,一般回收速度也比較快。
- 老年代GC(MajorGC/FullGC):指發(fā)生在老年代的GC,出現(xiàn)了MajorGC,經(jīng)常會(huì)伴隨至少一次的MinorGC(但非絕對(duì)的,在ParallelScavenge收集器的收集策略里就有直接進(jìn)行MajorGC的策略選擇過(guò)程)。MajorGC的速度一般會(huì)比MinorGC慢10倍以上。

