Java虛擬機05——對象分配與回收策略

對象的內存分配基本規律有如下幾條:java

  • 大多數狀況下就是在堆上分配(但也可能通過JIT編譯後被拆散爲標量類型並間接地棧上分配)。
  • 對象主要分配在新生代的Eden區上。
  • 若是啓動了本地線程分配緩衝,將按線程優先在TLAB上分配。
  • 少數狀況下也可能會直接分配在老年代中。

對象的分配規則不是百分百固定的,其細節取決於當前使用的是哪種垃圾收集組合,還有虛擬機中與內存相關的參數設置算法

對象優先在Eden分配

大多數狀況下,對象在新生代Eden區中分配。當Eden區沒有足夠空間進行分配時將發起一次Minor GC。安全

Minor GC指發生在新生代的垃圾收集動做測試

使用如下代碼進行測試:spa

public class ObjMemoryTest {
    private static  final int _1MB=1024*1024;
    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];
    }

    public static void main(String[] args) throws IOException {
        ObjMemoryTest.testAllocation();
    }
}
複製代碼

其中,須要設置參數線程

-verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC -Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8日誌

上述參數解釋以下:code

  • -verbose:gc -XX:+PrintGCDetails:打印GC詳細日誌信息。
  • -XX:+UseSerialGC:使用Serial收集器。
  • -Xms20m -Xmx20m:限制Java堆大小爲20MB。
  • -Xmn10m:新生代大小爲10MB。
  • -XX:SurvivorRatio=8:設置新生代中Eden區與一個Survivor區的空間比例是8:1

設置完後,Java堆共20M,新生代10M,老年代10M。其中新生代裏的Eden 8M,兩個Survivor各1M。代碼運行日誌以下:cdn

image.png

解釋:運行後新生代進行了GC回收,從8188K->714K。此次回收是給allocation4分配內存的時候,發現Edon區已經佔用了6M,剩餘空間已經不足分配allocation4的4M。因此執行了Minor GC,GC期間發現1M大小的Survivor沒法放入allocaiton1~3,因此只好經過分配擔保機制提早轉移到老年代去。對象

GC結束後,從GC日誌上能夠看到:4MB的allocation4被分配到Eden區,allocation1~3被分配到老年代中

大對象直接進入老年代

所謂的大對象是指須要大量連續內存空間的Java對象。虛擬機提供了一個-XX:PretenureSizeThreshold參數,大於這個設置值的對象將直接在老年代分配,從而避免在Eden區及兩個Survivor區之間發生大量的內存複製(新生代主要採用複製算法收集內存)。有如下的測試代碼: 其中,須要設置參數

-verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC -Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728

public static void testPretenureSizeThreshold() {
        byte[] allocation;
        allocation = new byte[4*_1MB];
    }
複製代碼

運行結果:

image.png

從結果上看老年代被使用了4M,而新生代幾乎沒有使用,這是由於PretenureSizeThreshold被設置成3MB(也就是3145728),所以超過3MB的對象會直接在老年代進行分配

長期存活的對象將進入老年代

虛擬機給每一個對象定義了一個對象年齡(Age)計數器,若是對象在Eden出生並通過第一次Minor GC後仍然存活,而且能被Survivor容納的話,對象年齡爲1,對象在Survivor區中熬過一次Minor GC,年齡增長1。當它的年齡增長到必定程度(默認15),就會被晉升到老年代中。晉升的閾值能夠經過參數-XX:MaxTenuringThreshold設置。實例代碼以下: 其中,須要設置參數

-verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC -Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8

public static void testMaxTenuredThreshold() {
        byte[] allocation1;
        byte[] allocation2;
        byte[] allocation3;
        byte[] allocation4;

        allocation1 = new byte[_1MB / 4];
        allocation2 = new byte[4 * _1MB];
        allocation3 = new byte[4 * _1MB];
        allocation3 = null;
        allocation4 = new byte[4 * _1MB];
    }
複製代碼

alocation1爲256kb內存,Survivor空間能夠容納,而allocation二、allocation3和allocation4須要4MB的空間,並不能被Survivor區容納。

當設置MaxTenuringThreshold = 1時,內存信息以下

image.png

因爲Eden區域的總大小是8MB,所以在分配allocation3時會由於Eden區空閒大小不夠而發生一次Minor GC操做,這時allocation1會被移入到Survivor區中,allocation2因Survivor區並不能容納會被提早提高到老年代。接下來在分配allocation3後分配allocation4還會觸發第二次Minor GC操做,此次操做因爲allocation1達到了晉升年齡,會被晉升到老年代,而allocation3會被回收,因此第二次Minor GC後新生代的已使用大小會變爲0K,最後allocation4會被分配到Eden區,所以獲得的最終內存空間的分配是Eden區使用51%(4MB+,用於存放allocation4),Survivor區域已使用全爲0,老年代已使用5059K(4MB+,用於存放allocation1和allocation2)。

而設置-XX:MaxTenuringThreshold=15後,將會獲得如下的結果:

image.png

注:若是在某些版本的JDK中不生效,能夠設置-XX:TargetSurvivorRatio=95參數調大Survivor區域的使用率

能夠看到Survivor區不爲空,這是因爲allocation1尚未被斷定爲長期存活的對象,還存在與Survivor區致使的。

動態年齡斷定

爲了能更好地適應不一樣程序的內存情況,若是在Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就能夠直接進入老年代,無須達到MaxTenuringThreshold中要求的年齡。

在執行下面的testMaxTenuredThreshold2()方法時,設置了-XX:MaxTenuringThreshold=15參數,會發現運行結果中Survivor的空間佔用仍然爲0%,而老年代比預期增長了11%,也就是說,allocation一、allocation2對象都直接進入了老年代,而沒有等到15歲的臨界年齡。由於這兩個對象加起來已經到達了512KB,而且它們是同年的,知足同年對象達到Survivor空間的一半規則。咱們只要註釋掉其中一個對象new操做,就會發現另一個就不會晉升到老年代中去了。

-verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC -Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:TargetSurvivorRatio=95

public static void testMaxTenuredThreshold2() {
        byte[] allocation1;
        byte[] allocation2;
        byte[] allocation3;
        byte[] allocation4;

        allocation1 = new byte[_1MB / 4];
        allocation2 = new byte[_1MB / 4];
        allocation3 = new byte[4 * _1MB];
        allocation4 = new byte[4 * _1MB];
        allocation4 = null;
        allocation4 = new byte[4 * _1MB];
    }
複製代碼

image.png

空間分配擔保

在發生Minor GC以前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代全部對象總空間,若是這個條件成立,那麼Minor GC能夠確保是安全的。若是不成立,則虛擬機會查看HandlePromotionFailure設置值是否容許擔保失敗。若是容許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,若是大於,將嘗試着進行一次Minor GC,儘管此次Minor GC是有風險的;若是小於,或者HandlePromotionFailure設置不容許冒險,那這時也要改成進行一次Full GC。

新生代使用複製收集算法,但爲了內存利用率,只使用其中一個Survivor空間來做爲輪換備份,所以當出現大量對象在Minor GC後仍然存活的狀況(最極端的狀況就是內存回收後新生代中全部對象都存活),就須要老年代進行分配擔保,把Survivor沒法容納的對象直接進入老年代。與生活中的貸款擔保相似,老年代要進行這樣的擔保,前提是老年代自己還有容納這些對象的剩餘空間,一共有多少對象會活下來在實際完成內存回收以前是沒法明確知道的,因此只好取以前每一次回收晉升到老年代對象容量的平均大小值做爲經驗值,與老年代的剩餘空間進行比較,決定是否進行Full GC來讓老年代騰出更多空間。

取平均值進行比較其實仍然是一種動態機率的手段,也就是說,若是某次Minor GC存活後的對象突增,遠遠高於平均值的話,依然會致使擔保失敗(HandlePromotion Failure)。若是出現了HandlePromotionFailure失敗,那就只好在失敗後從新發起一次Full GC。雖然擔保失敗時繞的圈子是最大的,但大部分狀況下都仍是會將HandlePromotionFailure開關打開,避免Full GC過於頻繁。

可是JDK 6 Update 24以後代碼中已經再也不使用HandlePromotionFailure,JDK 6 Update 24以後的規則變爲只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,不然將進行Full GC。

相關文章
相關標籤/搜索