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

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é)果:

162.png

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é)果:

163.png

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];
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末祷膳,一起剝皮案震驚了整個(gè)濱河市陶衅,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌直晨,老刑警劉巖搀军,帶你破解...
    沈念sama閱讀 211,817評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異勇皇,居然都是意外死亡罩句,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門儒士,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)的止,“玉大人,你說(shuō)我怎么就攤上這事着撩∽绺#” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵拖叙,是天一觀的道長(zhǎng)氓润。 經(jīng)常有香客問(wèn)我,道長(zhǎng)薯鳍,這世上最難降的妖魔是什么咖气? 我笑而不...
    開封第一講書人閱讀 56,498評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮挖滤,結(jié)果婚禮上崩溪,老公的妹妹穿的比我還像新娘。我一直安慰自己斩松,他們只是感情好伶唯,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著惧盹,像睡著了一般乳幸。 火紅的嫁衣襯著肌膚如雪瞪讼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評(píng)論 1 290
  • 那天粹断,我揣著相機(jī)與錄音符欠,去河邊找鬼。 笑死瓶埋,一個(gè)胖子當(dāng)著我的面吹牛希柿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播养筒,決...
    沈念sama閱讀 38,979評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼狡汉,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了闽颇?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,722評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤寄锐,失蹤者是張志新(化名)和其女友劉穎兵多,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體橄仆,經(jīng)...
    沈念sama閱讀 44,189評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡剩膘,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了盆顾。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片怠褐。...
    茶點(diǎn)故事閱讀 38,654評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖您宪,靈堂內(nèi)的尸體忽然破棺而出奈懒,到底是詐尸還是另有隱情,我是刑警寧澤宪巨,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布磷杏,位于F島的核電站,受9級(jí)特大地震影響捏卓,放射性物質(zhì)發(fā)生泄漏极祸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評(píng)論 3 313
  • 文/蒙蒙 一怠晴、第九天 我趴在偏房一處隱蔽的房頂上張望遥金。 院中可真熱鬧,春花似錦蒜田、人聲如沸稿械。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)溜哮。三九已至滔金,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間茂嗓,已是汗流浹背餐茵。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留述吸,地道東北人忿族。 一個(gè)月前我還...
    沈念sama閱讀 46,382評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像蝌矛,于是被迫代替她去往敵國(guó)和親道批。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容