空間分配擔(dān)保

為什么需要?

新生代采用的復(fù)制算法,留空一個(gè) survivor 作為空間備份,當(dāng)大量對(duì)象在 minor gc后仍然存活,survivor 無法放下,則會(huì)直接進(jìn)入老年代, 需要老年代的空間保證能容納得下這些對(duì)象。

如何擔(dān)保?

空間擔(dān)保比較的是 老年代最大的連續(xù)空閑空間 和 年輕代所有對(duì)象的內(nèi)存大小 或 歷次晉升到老年代的平均對(duì)象大小做比較

為什么需要是連續(xù)的空間?

分配擔(dān)保機(jī)制中,無論是新生代所有對(duì)象總和還是歷次晉升到老年代的平均大小的經(jīng)驗(yàn)值,在進(jìn)行比較時(shí)候都會(huì)和老年代最大連續(xù)空間進(jìn)行比較。 難道不連續(xù)只要湊起來夠就不行了?畢竟老年代還是 標(biāo)記-清理-(壓縮) 算法。

參考 https://gist.github.com/arrayadd/0ff0a468f1e201422d5264ac350f9ab1

確實(shí)不行,因?yàn)椋?/p>

  • 新生代的復(fù)制算法機(jī)制決定了需要連續(xù)空間。因?yàn)榉峙鋼?dān)保機(jī)制進(jìn)行時(shí)候,還沒有發(fā)生Minor GC,這時(shí)候經(jīng)過上次復(fù)制清理出來的空間,雖然分配了新對(duì)象,但這些對(duì)象很大程度上是連續(xù)的。一旦擔(dān)保成功,顯然直接復(fù)制過去最快速。
  • JVM中對(duì)象的分配和回收是非常高頻事情,直接決定其性能。如果為了追求理論上的合理性,來為每一個(gè)新生代晉升對(duì)象,在老年帶零碎的空間中尋找能放得下空間,將會(huì)是一件非常耗時(shí)繁瑣事情,況且同樣是一塊1Mb大小空間,到底是放一個(gè)0.8Mb的對(duì)象,還是繼續(xù)遍歷完所有晉升對(duì)象,比較后只為找一個(gè)更接近1Mb的。所以效率上是不允許。

一定有效嗎?

如果 最大連續(xù)空閑空間大于歷次晉升的平均大小 ,但是這次存活的對(duì)象突增,老年代放不下 只好在失敗后進(jìn)行一次 full gc

并不能減少 full gc ,該來的終歸會(huì)來的

代碼實(shí)驗(yàn)

public class PromotionTest {

    private static final Integer _1MB = 1024 * 1024;

    /**
     *  -XX:+UseSerialGC -Xmx15M -Xms15M -Xmn10M -XX:+PrintGCDetails
     *  每個(gè)對(duì)象 2M,minorgc 后 survivor放不下 肯定會(huì)晉升到老年代,不必等待年齡 方便實(shí)驗(yàn)
     * @param args
     */
    public static void main(String[] args) {
        byte[] a1, a2, a3, a4, a5, a6, a7, a8;
        System.out.println(" -----create a1------ ");
        a1 = new byte[2 * _1MB];
        System.out.println(" -----create a2------ ");
        a2 = new byte[2 * _1MB];
        System.out.println(" -----create a3------ ");
        a3 = new byte[2 * _1MB];

        System.out.println(" -----clear a1 ------ ");
        a1 = null;

        System.out.println(" -----create a4------ ");
        a4 = new byte[2 * _1MB];
        System.out.println(" -----create a5------ ");
        a5 = new byte[2 * _1MB];
        System.out.println(" -----create a6------ ");
        a6 = new byte[2 * _1MB];

        System.out.println(" -----clear a4 ------ ");
        a4 = null;
        System.out.println(" -----clear a5 ------ ");
        a5 = null;
        System.out.println(" -----clear a6 ------ ");
        a6 = null;

        System.out.println(" -----create a7------ ");
        a7 = new byte[2 * _1MB];
    }
}

gc日志分析

eden區(qū)8m,創(chuàng)建了3個(gè)2m對(duì)象后 占用的內(nèi)存超過6m,因?yàn)槊總€(gè)對(duì)象本身 還有對(duì)象頭等需要占用一點(diǎn)內(nèi)存;所以當(dāng)要?jiǎng)?chuàng)建第4個(gè)對(duì)象的時(shí)候 eden就放不下 需要進(jìn)行 minor gc了

執(zhí)行日志
-----create a1------
-----create a2------
-----create a3------
[GC (Allocation Failure) [DefNew: 6325K->543K(9216K), 0.0035420 secs] 6325K->4639K(15360K), 0.0035620 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
第一次 分配失敗
當(dāng)要?jiǎng)?chuàng)建 a4的時(shí)候,進(jìn)行 minor gc, 回收掉 a1,由于survivor放不下a2和a3,直接進(jìn)入老年代; 此時(shí):年輕代: 空, 老年代: a2, a3
9216K = eden + suvivor * 1
繼續(xù)創(chuàng)建 a4、a5、a6,都在年輕代中

-----clear a1 ------
-----create a4------
-----create a5------
-----create a6------
[GC (Allocation Failure) [DefNew: 6924K->6924K(9216K), 0.0000118 secs][Tenured: 4096K->4097K(6144K), 0.0023158 secs] 11020K->8654K(15360K), [Metaspace: 3026K->3026K(1056768K)], 0.0023555 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
**第二次 分配失敗
*創(chuàng)建 a7 的時(shí)候,eden放不下了,需要先進(jìn)行g(shù)c ,但是由于“分配擔(dān)?!睓C(jī)制
老年代目前 5m - 2m
2 放不下年輕代所有對(duì)象,也放不下之前晉升的 a2+a3的空間大小,擔(dān)保失敗了; jdk1.6 update24之后 handlePromotionFailure失效了,所以這邊故意把老年代設(shè)置成比較小的 5m

-----clear a4 ------
-----clear a5 ------
-----clear a6 ------
-----create a7------
[Full GC (Allocation Failure) [Tenured: 4097K->4549K(6144K), 0.0016231 secs] 10995K->4549K(15360K), [Metaspace: 3034K->3034K(1056768K)], 0.0016412 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
分配擔(dān)保失敗,進(jìn)行 full gc
老年代里邊的 a2 和 a3 還存活 內(nèi)存不變
年輕代的 a4、a5、a6 無引用了,回收掉;

Heap
def new generation total 9216K, used 2138K [0x00000007bf000000, 0x00000007bfa00000, 0x00000007bfa00000)
eden space 8192K, 26% used [0x00000007bf000000, 0x00000007bf216b60, 0x00000007bf800000)
from space 1024K, 0% used [0x00000007bf900000, 0x00000007bf900000, 0x00000007bfa00000)
to space 1024K, 0% used [0x00000007bf800000, 0x00000007bf800000, 0x00000007bf900000)
tenured generation total 6144K, used 4549K [0x00000007bfa00000, 0x00000007c0000000, 0x00000007c0000000)
the space 6144K, 74% used [0x00000007bfa00000, 0x00000007bfe717e0, 0x00000007bfe71800, 0x00000007c0000000)
Metaspace used 3050K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 333K, capacity 388K, committed 512K, reserved 1048576K

去掉分配擔(dān)保機(jī)制可以嗎?

從分配擔(dān)保的邏輯中,可以看出 分配擔(dān)保最關(guān)鍵作用是,在進(jìn)行Minor GC前是否需要根據(jù)歷屆晉升到老年代的平均值來發(fā)起一次Full GC. 換句話來說,去掉分配擔(dān)保(也就是相當(dāng)于參數(shù)HandlePromotionFailure=false)就意味著Full GC發(fā)生的幾率更大。

結(jié)論

分配擔(dān)保機(jī)制可以不要,但會(huì)導(dǎo)致Full GC更容易發(fā)生,進(jìn)而導(dǎo)致所謂的Stop The World,虛擬機(jī)短暫停止,吞吐量,性能下降。 有了分配擔(dān)保機(jī)制,就可以借鑒經(jīng)驗(yàn)值來減少Full GC這種耗時(shí)降低性能行為。

?著作權(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),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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