JVM 對象分配規則


對象的內存分配,從大方向上將,就是在堆上分配(但也可能經過JIT編譯后被拆散為標量類型並間接地在棧上分配),對象主要分配在新生代的Eden區上,如果啟動了本地線程分配緩沖,將按線程優先在TLAB上分配。少數情況也可能直接分配在老年代中,分配的規則並不是百分之百固定的,其細節取決於當前使用的是哪一種垃圾收集器組合,還有虛擬機中與內存相關的參數的設置。

 
打印內存情況代碼:
for (MemoryPoolMXBean memoryPoolMXBean : ManagementFactory.getMemoryPoolMXBeans()) {
    System.out.println(
        memoryPoolMXBean.getName() 
        + "\t總量:" + (memoryPoolMXBean.getUsage().getCommitted()/1024 + "K")
        + "\t使用的內存:" + (memoryPoolMXBean.getUsage().getUsed()/1024)  + "K");
}

  

 
 
一、對象優先分配在Eden區
 
測試代碼:
public class EdenTest {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        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
    }
} 

JVM參數:

-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
-XX:+PrintGCDetails
參數說明:
-Xms20M、-Xmx20M和-Xmn10M這3個參數限制Java堆大小為20M,且不可擴展,其中10MB分配給新生代,剩下的10M就分配給老年代了。-XX:SurvivorRatio=8決定了新生代中 Eden和Survivor區的空間比例為8:1
輸出結果:
上述PSYoungGen total 9216K=9M,為什么?因為有一塊Survivor區必須保證為空的。
 
 
二、大對象保存到老年代(優先保存在Eden區)
 
所謂大對象,就是指需要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串及數組(byte[]數組就是典型的大對象)。大對象對虛擬機的內存分配來說就是一個壞消息(更加壞的情況就是遇到一群朝生夕死的短命大對象,寫程序時應該避免),經常出現大對象容易導致內存還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來安置大對象。
虛擬機提供了一個-XX:PretenureSizeThreshold參數,令大於這個設置值的對象直接進入老年代中分配。這樣避免在Eden區及兩個Survivor區之間發生大量的內存拷貝。
注意: 實際測試中,加PretenureSizeThreshold這個參數並未啟到作用。所謂大的對象並不能說比PretenureSizeThreshold設置的參數大的稱為大對象,大的對象就是指很大的字符串,或者大的數組。
 
大對象進入老年代遵循以下原則:
(1)新生代內存空間充足時,保存在新生代,遵循對象優先分配原則,保存在Eden區;
(2)新生代內存空間不足時,直接進入到老年代;
(3) 當進行第一次GC時,無論內存是否充足,存活的大對象直接全部進入老年代;
為什么要優先保存在Eden?
試想一下,如果這個大的對象是朝生夕死的話,如果新生代足夠充足的話,完全沒有必要進入到老年代。
 
測試代碼:
public static void main(String[] args) {
    final int _1MB = 1024 * 1024;
    byte[] b1 = new byte[4 * _1MB];
    byte[] b2 = new byte[5 * _1MB];  
    // System.gc();
}

JVM參數:同上

輸出結果:
Heap
 PSYoungGen      total 9216K, used 4932K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 60% used [0x00000000ff600000,0x00000000ffad1028,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 10240K, used 5120K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 50% used [0x00000000fec00000,0x00000000ff100010,0x00000000ff600000)
 PSPermGen       total 21504K, used 2505K [0x00000000f9a00000, 0x00000000faf00000, 0x00000000fec00000)
  object space 21504K, 11% used [0x00000000f9a00000,0x00000000f9c72658,0x00000000faf00000)

上例中,新生代10M(Eden區8M)

當執行 b1 = new byte[4 * _1MB]; Eden區內存空間充足,直接保存在Eden區,此時Eden區還剩余4M空間;
當執行 b2 = new byte[5 * _1MB];  Eden區空間不足,所以直接保存到老年代。
 
 
 
三、長期存活的對象將進入老年代
這里使用對象到達一定的年齡直接進入老年代更准確,對象默認年齡為15;我們可以通過修改參數-XX:MaxTenuringThreshold來設置。
 
JVM參數:
-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails 
-XX:MaxTenuringThreshold=5
由於大對象在進行第一次GC的時候,會進入到老年代,所以這里不能使用大的數組來測試。因此也不好直接驗證

 

示例:

public class GCDemo {
    public static void main(String[] args) {
    	System.out.println("測試GC");
//        System.gc();
//        System.gc();
//        System.gc();
//        System.gc();
//        System.gc();
//        System.gc();
    }
}

執行上述代碼,在沒有進行GC時老年代空間使用率為 0%

在進行1~4次GC的時候,老年代空間使用率為 4%
在進行5次以上GC的時候,老年代空間使用率為 4%
 
 
 
4. 動態對象年齡判定
如果Survivor區中相同年齡的所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象可以直接進入老年代
為了能更好地適應不同程序的內存狀況,虛擬機並不總是要求對象的年齡必須達到MaxTenuringThreshold才能晉升到老年代,如果在Survivor空間中相同年齡所有對象大小的綜合大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。
 
 
五、空間分配擔保
當發生Minor GC時,虛擬機會檢測之前每次晉升到老年代的平均大小是否大於老年代的剩余空間大小,如果大於,則改為直接進行一次Full GC。如果小於,則查看HandlePromotionFailure設置是否允許擔保失敗;如果允許,那只會進行Minor GC;如果不允許,則也要改為進行一次Full GC。
 
新生代使用復制收集算法,但為了內存利用率,值使用其中一個Survivor空間來作為輪換備份,因此當出現大量對象在Minor GC后仍然存活的情況時(最極端就是內存回收后新生代中所有對象都存活),就需要老年代進行分配擔保, 讓Survivor無法容納的對象直接進入老年代。與生活中的貸款擔保類似,老年代要進行這樣的擔保,前提是老年代本身還有容納這些對象的剩余空間,一共有多少對象會活下去,在實際完成內存回收之前是無法明確知道的,所以只好取之前每一次回收晉升到老年代對象容量的平均大小值作為經驗,與老年代的剩余空間進行對比,決定是否進行Full GC來讓老年代騰出更多空間。
 
取平均值進行比較其實仍然是一種動態概率的手段,也就是說如果某次Minor GC存活后的對象突增,遠遠高於平均值時,依然會導致 擔保失敗(Handle Promotion Failure)。如果出現HandlePromotionFailure失敗,那就只好在失敗后重新發起一次Full GC。雖然擔保失敗時繞的圈子是最大的,但大部分情況下都還是會將HandlePromotionFailure開關打開,避免Full GC過於頻繁。
 
 
 
 
 
 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM