JVM學習-java垃圾回收(三)-內存分配

在上一篇中主要說到了垃圾回收算法、垃圾收集器等,這篇我們來學習下對象在內存分配的策略;

一、概述

對象在內存分配,一般都是在堆內存上分配,主要是會分配到新生代的Eden區,如果有開啓本地線程分配緩衝區,會按線程優先在TLAB上分配,還有少數情況會直接在老年代分配;

對象內存的分配的策略細節由垃圾收集器組合決定,還有許多虛擬機參數可以由我們控制設置;這裏我們主要看下幾天普遍的分配規則,並在 Serial + Serial Old 組合驗證下,其他組合的垃圾收集器也基本相識;

二、對象優先在Eden分配

一般情況下,對象在新生代Eden區分配,當Eden區空間不足是,虛擬機可能會發起一次Minor GC;

Minor GC - 新生代GC,指發送在新生代的GC,非常頻繁,回收速度較快
Major GC(Full GC) - 老年代GC,出現Major GC 一般都伴隨至少一次的Minor GC(不是一定),而且Major GC 一般都比Minor GC 要慢的多;

通過測試代碼驗證下這個規則:

    private static final int _1MB = 1024 * 1024;
    /**
     * VM參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC
     */
    public static void testEdenAllocation() {
        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];  // 出現一次Minor GC
     }

通過VM設置 堆內存大小爲20M,老年代10M,Eden區與一個Survivor區的比例是 8:1;所以新生代可用空間應該爲9M(9216K)(Eden + 一個Survivor),兩個Survivor區分別佔用1M;
測試代碼中,開始創建3個2M對象,此時新生代可用空間應該剩餘3M,當創建最後一個4M的對象時,可用空間不足,會發生一次Minor GC,開始創建的3個2M對象不會被回收,而是被移到老年代;代碼執行完時,新生代使用了4M,而老年代使用了6M;

程序運行完打印出來的GC日誌可以說明:

[GC[DefNew: 6824K->472K(9216K), 0.0071169 secs] 6824K->6616K(19456K), 0.0071828 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
Heap
 def new generation   total 9216K, used 4896K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
  eden space 8192K,  54% used [0x00000000f9a00000, 0x00000000f9e51fa0, 0x00000000fa200000)
  from space 1024K,  46% used [0x00000000fa300000, 0x00000000fa376348, 0x00000000fa400000)
  to   space 1024K,   0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)
 tenured generation   total 10240K, used 6144K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
   the space 10240K,  60% used [0x00000000fa400000, 0x00000000faa00030, 0x00000000faa00200, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2551K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  12% used [0x00000000fae00000, 0x00000000fb07de20, 0x00000000fb07e000, 0x00000000fc2c0000)
No shared spaces configured.

從GC日誌說明,
- Eden區空間8M 使用4M(eden space 8192K, 54% used)
- 兩個Survivor空間各佔1M空間,(from space 1024K, 46% used )(to space 1024K, 0% used )
- 新生代空間9M,使用4M( def new generation total 9216K, used 4896K)
- 老年代空間1M,使用6M(tenured generation total 10240K, used 6144K)

二、大對象直接進入老年代

所謂大對象即是需要佔用較大連續內存空間的對象,如超長的字符串,大容量基本類型數組等;大對象對於虛擬機的內存分配是不利的,特別是大量的“臨時”大對象,虛擬機可能爲了給這個對象分配內存而需要更加頻繁的GC;

在Serial 和 ParNew 收集器下,虛擬機可以通過-XX:PretenureSizeThreshold參數設置對象直接進入老年代分配的閥值;大對象直接進入老年代可以避免在Eden區和兩個Survivor區發生大量的內存複製;

通過以下測試代碼驗證,設置-XX:PretenureSizeThreshold=3145728 (這個參數單位是 byte)

    private static final int _1MB = 1024 * 1024;
    /**
     * -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails
     * -XX:SurvivorRatio=8 -XX:+UseSerialGC
     * -XX:PretenureSizeThreshold=3145728
     */
    public static void testBigObjAllocation() {
        byte[] allocation;
        allocation = new byte[4 * _1MB];
    }

如果參數有效,測試代碼中創建一個4M的對象,應該直接在老年代分配,通過GC日誌驗證:

Heap
 def new generation   total 9216K, used 999K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
  eden space 8192K,  12% used [0x00000000f9a00000, 0x00000000f9af9f70, 0x00000000fa200000)
  from space 1024K,   0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)
  to   space 1024K,   0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
 tenured generation   total 10240K, used 4096K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
   the space 10240K,  40% used [0x00000000fa400000, 0x00000000fa800010, 0x00000000fa800200, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2548K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  11% used [0x00000000fae00000, 0x00000000fb07d388, 0x00000000fb07d400, 0x00000000fc2c0000)
No shared spaces configured.

可以看出,老年代佔用了4M (tenured generation total 10240K, used 4096K)

三、長期存活對象進入老年代

虛擬機爲每個對象都保存一個年齡計算器,如果對象在Eden區被首次分配,然後進過一次Minor GC後然後可以存活,並且Survivor有足夠空間區容納,將被遷移到Survivor區,對象年齡加一;
對象進入老年代的年齡閥值,可以通過參數 -XX:MaxTenuringThreshold 設置;爲了測試這個規則以及參數,我們設置-XX:MaxTenuringThreshold 值爲1和10進行測試:

    /**
     * -verbose:gc -Xms40M -Xmx40M -Xmn20M -XX:+PrintGCDetails
     * -XX:SurvivorRatio=8 -XX:+UseSerialGC
     * -XX:MaxTenuringThreshold=1(10)
     */
    public static void testTrnuringThreshold() {
        byte[] allocation1, allocation2, allocation3;
        allocation1 = new byte[ _1MB / 8];
        allocation2 = new byte[8 * _1MB];
        System.out.println("after all 2");
        allocation3 = new byte[8 * _1MB];
        System.out.println("after all 3");
        allocation3 = null;
        allocation3 = new byte[8 * _1MB];
        System.out.println("after all 4");
    }

當參數值爲1時,得到結果:

after all 2
[GC[DefNew: 9303K->602K(18432K), 0.0043713 secs] 9303K->8794K(38912K), 0.0044024 secs] [Times: user=0.00 sys=0.02, real=0.00 secs] 
after all 3
[GC[DefNew: 9466K->0K(18432K), 0.0011212 secs] 17658K->8793K(38912K), 0.0011374 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
after all 4
Heap
 def new generation   total 18432K, used 8472K [0x00000000f8600000, 0x00000000f9a00000, 0x00000000f9a00000)
  eden space 16384K,  51% used [0x00000000f8600000, 0x00000000f8e460f8, 0x00000000f9600000)
  from space 2048K,   0% used [0x00000000f9600000, 0x00000000f9600140, 0x00000000f9800000)
  to   space 2048K,   0% used [0x00000000f9800000, 0x00000000f9800000, 0x00000000f9a00000)
 tenured generation   total 20480K, used 8793K [0x00000000f9a00000, 0x00000000fae00000, 0x00000000fae00000)
   the space 20480K,  42% used [0x00000000f9a00000, 0x00000000fa296598, 0x00000000fa296600, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2552K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  12% used [0x00000000fae00000, 0x00000000fb07e140, 0x00000000fb07e200, 0x00000000fc2c0000)
No shared spaces configured.

參數值爲10結果:

after all 2
[GC[DefNew: 9303K->602K(18432K), 0.0047286 secs] 9303K->8794K(38912K), 0.0047632 secs] [Times: user=0.00 sys=0.02, real=0.00 secs] 
after all 3
[GC[DefNew: 9466K->601K(18432K), 0.0011722 secs] 17658K->8793K(38912K), 0.0011949 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
after all 4
Heap
 def new generation   total 18432K, used 9073K [0x00000000f8600000, 0x00000000f9a00000, 0x00000000f9a00000)
  eden space 16384K,  51% used [0x00000000f8600000, 0x00000000f8e460f8, 0x00000000f9600000)
  from space 2048K,  29% used [0x00000000f9600000, 0x00000000f96966c8, 0x00000000f9800000)
  to   space 2048K,   0% used [0x00000000f9800000, 0x00000000f9800000, 0x00000000f9a00000)
 tenured generation   total 20480K, used 8192K [0x00000000f9a00000, 0x00000000fae00000, 0x00000000fae00000)
   the space 20480K,  40% used [0x00000000f9a00000, 0x00000000fa200010, 0x00000000fa200200, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2552K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  12% used [0x00000000fae00000, 0x00000000fb07e140, 0x00000000fb07e200, 0x00000000fc2c0000)
No shared spaces configured.

從結果可以看出,當-XX:MaxTenuringThreshold=1時,allocation1 對象第二次GC時進入老年代,新生代使用空間變爲0;
當-XX:MaxTenuringThreshold=10時,allocation1 對象兩次GC後還在新生代Survivor1(from space 2048K, 29% used);

四、動態對象年齡判斷

上面說到對象達到一定的年齡將進入老年代(-XX:MaxTenuringThreshold 參數設定),但是虛擬機不一定是在只有對象達到年齡閥值纔將對象放入老年代,還有動態判斷規則:當Survivor區中的相同年齡對象的大小達到Survivor區空間的一半以上,也會經這個對象直接放入老年代,不會等到-XX:MaxTenuringThreshold閥值;

    /**
     * -verbose:gc -Xms40M -Xmx40M -Xmn20M -XX:+PrintGCDetails
     * -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:+PrintHeapAtGC
     * -XX:MaxTenuringThreshold=10
     */
    public static void testTrnuringThreshold2() {
        byte[] allocation1, allocation2, allocation3,allocation4;
        allocation1 = new byte[ _1MB / 2];
        allocation2 = new byte[ _1MB / 2];
        allocation3 = new byte[8 * _1MB];
        allocation4 = new byte[8 * _1MB];
        allocation4 = null;
        allocation4 = new byte[8 * _1MB];
    }

爲了更加直觀的觀察from Survivor 區的變化,可以加上參數-XX:+PrintHeapAtGC,每次GC前後都打印堆內存情況
從運行結果可以看到,兩次GC後 Survivor區是 0k,說明allocation1、allocation2 對象並沒有等到年齡閥值才計入老年代:

{Heap before GC invocations=0 (full 0):
 def new generation   total 18432K, used 10199K [0x00000000f8600000, 0x00000000f9a00000, 0x00000000f9a00000)
  eden space 16384K,  62% used [0x00000000f8600000, 0x00000000f8ff5da8, 0x00000000f9600000)
  from space 2048K,   0% used [0x00000000f9600000, 0x00000000f9600000, 0x00000000f9800000)
  to   space 2048K,   0% used [0x00000000f9800000, 0x00000000f9800000, 0x00000000f9a00000)
 tenured generation   total 20480K, used 0K [0x00000000f9a00000, 0x00000000fae00000, 0x00000000fae00000)
   the space 20480K,   0% used [0x00000000f9a00000, 0x00000000f9a00000, 0x00000000f9a00200, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2542K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  11% used [0x00000000fae00000, 0x00000000fb07bb80, 0x00000000fb07bc00, 0x00000000fc2c0000)
No shared spaces configured.
[GC[DefNew: 10199K->1497K(18432K), 0.0066659 secs] 10199K->9689K(38912K), 0.0066924 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap after GC invocations=1 (full 0):
 def new generation   total 18432K, used 1497K [0x00000000f8600000, 0x00000000f9a00000, 0x00000000f9a00000)
  eden space 16384K,   0% used [0x00000000f8600000, 0x00000000f8600000, 0x00000000f9600000)
  from space 2048K,  73% used [0x00000000f9800000, 0x00000000f99765c8, 0x00000000f9a00000)
  to   space 2048K,   0% used [0x00000000f9600000, 0x00000000f9600000, 0x00000000f9800000)
 tenured generation   total 20480K, used 8192K [0x00000000f9a00000, 0x00000000fae00000, 0x00000000fae00000)
   the space 20480K,  40% used [0x00000000f9a00000, 0x00000000fa200010, 0x00000000fa200200, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2542K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  11% used [0x00000000fae00000, 0x00000000fb07bb80, 0x00000000fb07bc00, 0x00000000fc2c0000)
No shared spaces configured.
}
{Heap before GC invocations=1 (full 0):
 def new generation   total 18432K, used 10186K [0x00000000f8600000, 0x00000000f9a00000, 0x00000000f9a00000)
  eden space 16384K,  53% used [0x00000000f8600000, 0x00000000f8e7c410, 0x00000000f9600000)
  from space 2048K,  73% used [0x00000000f9800000, 0x00000000f99765c8, 0x00000000f9a00000)
  to   space 2048K,   0% used [0x00000000f9600000, 0x00000000f9600000, 0x00000000f9800000)
 tenured generation   total 20480K, used 8192K [0x00000000f9a00000, 0x00000000fae00000, 0x00000000fae00000)
   the space 20480K,  40% used [0x00000000f9a00000, 0x00000000fa200010, 0x00000000fa200200, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2545K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  11% used [0x00000000fae00000, 0x00000000fb07c708, 0x00000000fb07c800, 0x00000000fc2c0000)
No shared spaces configured.
[GC[DefNew: 10186K->0K(18432K), 0.0016216 secs] 18378K->9689K(38912K), 0.0016353 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap after GC invocations=2 (full 0):
 def new generation   total 18432K, used 0K [0x00000000f8600000, 0x00000000f9a00000, 0x00000000f9a00000)
  eden space 16384K,   0% used [0x00000000f8600000, 0x00000000f8600000, 0x00000000f9600000)
  from space 2048K,   0% used [0x00000000f9600000, 0x00000000f9600100, 0x00000000f9800000)
  to   space 2048K,   0% used [0x00000000f9800000, 0x00000000f9800000, 0x00000000f9a00000)
 tenured generation   total 20480K, used 9688K [0x00000000f9a00000, 0x00000000fae00000, 0x00000000fae00000)
   the space 20480K,  47% used [0x00000000f9a00000, 0x00000000fa376350, 0x00000000fa376400, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2545K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  11% used [0x00000000fae00000, 0x00000000fb07c708, 0x00000000fb07c800, 0x00000000fc2c0000)
No shared spaces configured.
}
Heap
 def new generation   total 18432K, used 8356K [0x00000000f8600000, 0x00000000f9a00000, 0x00000000f9a00000)
  eden space 16384K,  51% used [0x00000000f8600000, 0x00000000f8e28fd0, 0x00000000f9600000)
  from space 2048K,   0% used [0x00000000f9600000, 0x00000000f9600100, 0x00000000f9800000)
  to   space 2048K,   0% used [0x00000000f9800000, 0x00000000f9800000, 0x00000000f9a00000)
 tenured generation   total 20480K, used 9688K [0x00000000f9a00000, 0x00000000fae00000, 0x00000000fae00000)
   the space 20480K,  47% used [0x00000000f9a00000, 0x00000000fa376350, 0x00000000fa376400, 0x00000000fae00000)
 compacting perm gen  total 21248K, used 2552K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
   the space 21248K,  12% used [0x00000000fae00000, 0x00000000fb07e268, 0x00000000fb07e400, 0x00000000fc2c0000)
No shared spaces configured.

可以試下把allocation2 對象創建註釋掉運行,會發現結果Survivor區並不是爲0 k,應爲沒有符合到規則,相同年齡對象大小佔比一半以上;

五、空間分配擔保

在發生Minor GC 時,如果空間不足會導致對象直接進入老年代,但是也有可能存在老年代空間也不足的情況;在每次Minor GC前,虛擬機會檢查老年代的最大可用空間是否大於新生代對象大小的總和,如果是,則認爲這次Minor GC 是安全的,
否則就檢查是否允許擔保失敗(HandlerPromotionFailure 參數),如果允許,則判斷老年代最大連續可用空間是否大於歷次Minor GC 平均晉升老年代對象的大小,如果大於,則會嘗試一次Minor GC;如果小於或者不允許,則觸發一次Full GC;

取平均的方式是屬於一種冒險判斷,可能會出現某一次Minor GC存在大量對象存活,導致老年代擔保失敗,不得不進行 一次Full GC,默認情況下 HandlerPromotionFailure 參數是打開的,而且在JDK7 後,這個參數已經是無效的,虛擬機都會進行冒險擔保,因爲這樣可以有效減少FUll GC,而且突然出現某一次Minor GC 存活大量對象的概率也比較小;

本篇主要學習GC 中內存分配的幾條常見規則,並且基於Serial + Serial Old收集器進行了一些測試代碼驗證;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章