18-動(dòng)態(tài)對(duì)象年齡判斷+空間分配擔(dān)保規(guī)則+老年代回收算法

動(dòng)態(tài)對(duì)象年齡判斷

本文中用到的案例是接著上一篇文章繼續(xù)的,如果有不清楚同學(xué)請(qǐng)先查看上一篇文章

為了能更好地適應(yīng)不同程序的內(nèi)存狀況,HotSpot虛擬機(jī)并不是永遠(yuǎn)要求對(duì)象的年齡必須達(dá)到- XX:MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對(duì)象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對(duì)象就可以直接進(jìn)入老年代,無(wú)須等到-XX:MaxTenuringThreshold中要求的年齡。

我們來(lái)看執(zhí)行后的內(nèi)存情況:

Heap
 def new generation   total 9216K, used 4316K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  52% used [0x00000000fec00000, 0x00000000ff037008, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff4002d8, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 4949K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  48% used [0x00000000ff600000, 0x00000000ffad5400, 0x00000000ffad5400, 0x0000000100000000)
 Metaspace       used 3265K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 354K, capacity 388K, committed 512K, reserved 1048576K

老年代占比有48%,比預(yù)期只有allocation2對(duì)象占比40%多出了8%,那么也就是說(shuō)allocation1和allocation2對(duì)象都直接進(jìn)入了老年代,并沒(méi)有等到15歲的臨界年齡。因?yàn)檫@兩個(gè)對(duì)象加起來(lái)已經(jīng)達(dá)到了4.25MB, 并且它們是同年齡的,滿足同年對(duì)象達(dá)到Survivor空間一半的規(guī)則。

我們來(lái)通過(guò)以下案例代碼來(lái)說(shuō)明鞏固:

    private static final int _1MB = 1024 * 1024;

    public static void testTenuringThreshold() {
        byte[] allocation1, allocation2, allocation3,allocation4;
        allocation1 = new byte[_1MB / 4]; // allocation1+allocation2大于survivo空間一半
        allocation2 = new byte[_1MB / 4];
        allocation3 = new byte[4 * _1MB];
        allocation4 = new byte[4 * _1MB];
        allocation4 = null;
        allocation4 = new byte[4 * _1MB];
    }

allocation1對(duì)象和allocation2對(duì)象以及allocation3對(duì)象都可以存放進(jìn)Eden區(qū),當(dāng)allocation4對(duì)象申請(qǐng)分配的時(shí)候空間不足,這時(shí)進(jìn)行第一次GC回收:(這里不再體現(xiàn)系統(tǒng)的一些對(duì)象占用)

image

allocation1對(duì)象和allocation2對(duì)象進(jìn)入s1區(qū),大對(duì)象allocation3直接進(jìn)入老年代:

image

接著執(zhí)行最后兩段代碼:

allocation4 = null;
allocation4 = new byte[4 * _1MB];

觸發(fā)第二次GC,但是由于a1+a2這兩個(gè)對(duì)象加起來(lái)已經(jīng)到達(dá)了512KB,并且它們是同年齡的,滿足同年對(duì)象達(dá)到Survivor空間一半的規(guī)則。根據(jù)動(dòng)態(tài)年齡判斷規(guī)則,這時(shí)直接進(jìn)入老年代:

image

空間分配擔(dān)保

之前我們講過(guò),如果Eden區(qū)中的對(duì)象無(wú)法存入Survivor區(qū)則會(huì)通過(guò)空間分配擔(dān)保,讓對(duì)象直接進(jìn)入老年代。

But!大家是否想過(guò)一個(gè)問(wèn)題:如果老年代里空間也不夠這些對(duì)象呢?又該咋整!別急,我們一步一圖繼續(xù)講解。

老年代空間夠用

首先:在發(fā)生Minor GC之前,虛擬機(jī)必須先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對(duì)象總空間,如果這個(gè)條件成立,那這一次Minor GC可以確保是安全的。

試想一個(gè)極端情況就是MinorGC后所有對(duì)象存活下來(lái),那所有的對(duì)象都會(huì)進(jìn)入老年代,如果老年代判斷剩余空間是大于所有對(duì)象的那么就可以放心擔(dān)保進(jìn)入老年代

老年代空間不夠

但是:假如執(zhí)行Minor GC之前,發(fā)現(xiàn)老年代的可用內(nèi)存空間已經(jīng)小于新生代的全部對(duì)象大小了,那么這個(gè)時(shí)候就有可能新生代Minor GC后對(duì)象全部存活,然后需要轉(zhuǎn)移到老年代,但是老年代空間又不夠的情況。(理論上是有這種可能得)因此JVM在Minor GC之前,當(dāng)判斷到老年代的可用內(nèi)存已經(jīng)小于新生代的全部對(duì)象大小,會(huì)看一個(gè)參數(shù):“-XX:HandlePromotionFailure”是否設(shè)置了。如果有該參數(shù)的設(shè)置,那會(huì)繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對(duì)象的平均大小。當(dāng)判斷到歷次平均大小是小于老年代可用內(nèi)存空間的,將嘗試進(jìn)行一次Minor GC,盡管這次Minor GC是有風(fēng)險(xiǎn)的;如果小于,或者-XX:HandlePromotionFailure沒(méi)有設(shè)置,那這時(shí)就要改為進(jìn)行一次Full GC。

舉個(gè)栗子,之前每次Minor GC之后,平均都有10MB左右的對(duì)象會(huì)進(jìn)入老年代,那么此時(shí)老年代可用內(nèi)存大于10MB,這就說(shuō)明,很可能這次Minor GC過(guò)后也是差不多10MB左右的對(duì)象會(huì)進(jìn)入老年代,此時(shí)老年代空間是夠的。

取歷史平均值來(lái)比較其實(shí)仍然是一種賭概率的解決辦法,也就是說(shuō)假如某次Minor GC存活后的對(duì)象突增,遠(yuǎn)遠(yuǎn)高于歷史平均值的話,依然會(huì)導(dǎo)致?lián)J?。如果出現(xiàn)了擔(dān)保失敗,那就只好老老實(shí)實(shí)地重新發(fā)起一次Full GC,這樣停頓時(shí)間就很長(zhǎng)了。雖然擔(dān)保失敗時(shí)繞的圈子是最大的,但通常情況下都還是會(huì)將-XX:HandlePromotionFailure開(kāi)關(guān)打開(kāi),避免Full GC過(guò)于頻繁。

我們通過(guò)完整的一張流程圖來(lái)幫助大家更好的梳理清楚整個(gè)JVM的空間擔(dān)保原則:

image

小結(jié): 通過(guò)以上的分析我們其實(shí)也知道了,老年代觸發(fā)垃圾回收的時(shí)機(jī),一般就是兩個(gè):

  1. Minor GC之前發(fā)現(xiàn)要進(jìn)入老年代的對(duì)象太多,裝不下,觸發(fā)Fu'll GC 再帶著進(jìn)行Minor GC
  2. Mionr GC過(guò)后,剩余對(duì)象太多老年代存放不下,觸發(fā)Full GC

老年代垃圾回收算法-標(biāo)記整理算法

那么對(duì)于老年代的垃圾回收采用的是什么算法呢?

標(biāo)記-復(fù)制算法在對(duì)象存活率較高時(shí)就要進(jìn)行較多的復(fù)制操作,效率將會(huì)降低。更關(guān)鍵的是,如果 不想浪費(fèi)50%的空間,就需要有額外的空間進(jìn)行分配擔(dān)保,以應(yīng)對(duì)被使用的內(nèi)存中所有對(duì)象都100%存 活的極端情況,所以在老年代一般不能直接選用這種算法。

針對(duì)老年代對(duì)象的存亡特征,1974年Edward Lueders提出了另外一種有針對(duì)性的“標(biāo)記-整 理”(Mark-Compact)算法,其中的標(biāo)記過(guò)程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向內(nèi)存空間一端移動(dòng),然后直接清理掉邊界以外的內(nèi)存,“標(biāo)記-整理”算法的示意圖如下圖所示。

image

標(biāo)記-清除算法與標(biāo)記-整理算法的本質(zhì)差異在于前者是一種非移動(dòng)式的回收算法,而后者是移動(dòng)式的。是否移動(dòng)回收后的存活對(duì)象是一項(xiàng)優(yōu)缺點(diǎn)并存的風(fēng)險(xiǎn)決策:

如果移動(dòng)存活對(duì)象,尤其是在老年代這種每次回收都有大量對(duì)象存活區(qū)域,移動(dòng)存活對(duì)象并更新 所有引用這些對(duì)象的地方將會(huì)是一種極為負(fù)重的操作,而且這種對(duì)象移動(dòng)操作必須全程暫停用戶應(yīng)用 程序才能進(jìn)行,這就更加讓使用者不得不小心翼翼地權(quán)衡其弊端了,像這樣的停頓被最初的虛擬機(jī) 設(shè)計(jì)者形象地描述為“Stop The World”。

老年代的垃圾回收算法速度至少比新聲代的垃圾回收算法的速度慢10倍!如果頻繁出現(xiàn)老年代的Full GC,會(huì)導(dǎo)致系統(tǒng)性能被嚴(yán)重影響,出現(xiàn)頻繁卡頓的情況!

所有后面用各種案例給大家展現(xiàn)出來(lái)的就是在各種業(yè)務(wù)系統(tǒng)的生產(chǎn)故障下,如何去一步一步分析為什么會(huì)頻繁觸發(fā)Full GC,然后怎么通過(guò)調(diào)整JVM的參數(shù)來(lái)進(jìn)行優(yōu)化!

如果大家透徹的理解了最近幾篇文章涵蓋的JVM運(yùn)行原理,就應(yīng)該能明白,所謂 JVM 優(yōu)化就是盡可能的讓對(duì)象都在新生代分配和回收,盡量避免頻繁的老年代Full GC ,同時(shí)給系統(tǒng)充足的內(nèi)存大小,避免新生代也頻繁的垃圾回收,更好的保證系統(tǒng)的運(yùn)行效率。

關(guān)于如何優(yōu)化JVM,后續(xù)會(huì)有大量的案例帶著大家去實(shí)戰(zhàn)。

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

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

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