1. 程式人生 > >JVM理論:(二/1)內存分配策略

JVM理論:(二/1)內存分配策略

本地線程 準備 最大 機會 bubuko 空間不夠 嘗試 它的 日誌分析

  Java技術體系中所提倡的自動內存管理最終可以歸結為自動化地解決兩個問題:給對象分配內存以及回收分配給對象的內存。

對象的分配可能有以下幾種方式:

1、JIT編譯後被拆散為標量類型並間接地棧上分配

2、對象主要分配在新生代的Eden區上,如果啟動了本地線程分配緩沖,將按線程優先在TLAB上分配

3、少數情況下也會直接分配在老年代

參考下圖:

  技術分享圖片

5種內存分配策略

1、對象優先在Eden分配

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

  先來看看兩種GC類型的定義

  新生代GC(Minor GC):指發生在新生代的垃圾收集動作,因為Java對象大多具備朝生夕死的特性,所以Minor GC非常頻繁,一般回收速度也比較快。

  老年代GC(Major GC/Full GC):指發生在老年代的GC,出現了Major GC,經常會伴隨至少一次的Minor GC(Parallel Scavenge裏可設置直接進行Major GC,所以並非絕對),Major GC的速度一般會比Minor GC慢10倍以上。 

經典案例代碼:
private
static final int _1MB = 1024 * 1024; /** * VM參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 */ 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]; // 出現一次Minor GC } 運行結果: [GC [DefNew: 6651K->148K(9216K), 0.0070106 secs] 6651K->6292K(19456K), 0.0070426 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

  先看看設置的虛擬機參數,-Xms20M -Xmx20M -Xmn10M這3個參數限制了Java堆大小為20MB,不可擴展,其中10MB分配給新生代,剩下的10MB分配給老年代。-XX:SurvivorRatio=8決定了新生代中Eden區與一個Survivor區的空間比例是8:1,所以Eden有8MB的空間,一個Survivor區有1MB的空間,-XX:+PrintGCDetails表示虛擬機會在垃圾回收時打印內存回收日誌。

  testAllocation()方法中嘗試分配3個2MB大小和1個4MB大小的對象,在執行分配allocation4對象的語句時,會發現Eden區已經被占用了6MB,剩余2M空間,已不足以分配allocation4所需的4MB內存,因此會發生一次Minor GC。GC期間虛擬機又發現已有的3個2MB大小的對象全部無法放入只有1MB大小的Survivor空間,所以根據分配擔保機制會提前轉移到老年代去。

  根據打印的GC日誌,6651K->148K(9216K), 6651K->6292K(19456K)也可以看出,新生代從6651K變為148K,但總內存占用量幾乎沒有減少,也證實allocation1、allocation2、allocation3三個對象都是存活的,只是被轉移到了老年代,虛擬機幾乎沒有找到可回收的對象。這次GC後,程序執行完的結果是,Eden被allocation4占用4MB,Survivor空閑,老年代被allocation1、allocation2、allocation3三個對象占用6MB。

  所以,對象在新生代Eden區中分配,當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC。

*//堆空間分布日誌
Heap  
  def new generation   total 9216K, used 4326K    //年輕代分布   
  eden space 8192K,  51% used ≈ 4MB
  from space 1024K,  14% used ≈ 148KB,應該是之前的垃圾,忽略  
  to space 1024K,   0% used
  tenured generation   total 10240K, used 6144K    //老年代分布  
  the space 10240K,  60% used ≈ 2048KB*3
  compacting perm gen  total 12288K, used 2114K    //永久代(方法區)分布,本例不考慮  
  the space 12288K,  17% used

  再從上例中的內存對分布的角度來重新推測過程:當分配到allocation4時,發現eden空間不足,這時進行GC,但由於Survivor只有1MB,存不下allocation1、allocation2、allocation3中的任意對象,所以這三個對象都直接進入老年代,最後allocation4分配在Eden,占4MB。

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

  大對象是指需要大量連續內存空間的Java對象,典型大對象有長字符串和數組,大對象對虛擬機的內存分配來說是一個壞消息,寫程序時還要避免創建一些朝生夕死的短命大對象,經常出現大對象容易導致內存還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來安置他們。

  虛擬機提供-XX:PretenureSizeThreshold參數,令大於這個設置值的對象直接在老年代分配,這樣避免再Eden及兩個Survivor區間發生大量的內存復制,這個參數只對Serial和ParNew兩款收集器有效,如果遇到必須使用此參數的場合,可以考慮ParNew加CMS的組合。

3、根據對象年齡判定進入老年代

  虛擬機給每個對象定義了一個對象年齡計數器,如果對象在Eden出生並經過第一次Minor GC後仍然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,並且對象年齡設為1,對象在Survivor區中每熬過一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(默認為15歲),就會被晉升到老年代中,可以通過-XX:MaxTenuringThreshold參數來設置年齡閾值。

代碼示例:
private static final int _1MB = 1024 * 1024;
/**
 * VM參數: -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails
 * -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:MaxTenuringThreshold=1
 * -XX:+PrintTenuringDistribution
 */
@SuppressWarnings("unused")
public static void testTenuringThreshold() {
    byte[] allocation1, allocation2, allocation3;
    allocation1 = new byte[_1MB / 4];
    //什麽時候進入老年代決定於XX:MaxTenuringThreshold設置
    allocation2 = new byte[4 * _1MB];
    allocation3 = new byte[4 * _1MB];
    allocation3 = null;
    allocation3 = new byte[4 * _1MB];
}
堆空間分布日誌
以MaxTenuringThreshold=1參數來運行的結果:
Heap  
def new generation   total 9216K, used 4178K   //年輕代分布
eden space 8192K,  51% used ≈ 4MB 
from space 1024K,   0% used  
to   space 1024K,   0% used    
tenured generation   total 10240K, used 4500K  //老年代分布
the space 10240K,  43% used ≈ 4MB+256KB
compacting perm gen  total 12288K, used 2114K  //永久代分布先忽略
the space 12288K,  17% used
 以MaxTenuringThreshold=1的情況來分析,當要分配allocation3時,Eden空間不足,準備開始GC,照理Survivor(1MB)是能容納下allocation1(256KB)的,但因為MaxTenuringThreshold=1,allocation1對象在第二次GC發生時進入老年代,所以Survivor區沒有對象,老年代存放了
allocation1和allocation2,eden中存的是allocation3。
以MaxTenuringThreshold=15參數來運行的結果:
Heap  
def new generation   total 9216K, used 4582K   //年輕代分布
eden space 8192K,  51% used ≈ 4MB 
from space 1024K,  39% used ≈ 256KB 
to   space 1024K,   0% used  
tenured generation   total 10240K, used 4096K  //老年代分布
the space 10240K,  40% used = 4MB
 以MaxTenuringThreshold=15的情況來分析,當要分配allocation3時,Eden空間不足,準備開始GC,這次MaxTenuringThreshold=15,所以allocation1不會直接進入老年代,會到Survivor區域內,Survivor存不了allocation2,所以allocation2被移動到老年代,最後在Eden分配allocation3。

4、Survivor空間中相同年齡的對象過半直接進入老年代

  上一點講到MaxTenuringThreshold參數,但虛擬機並不是永遠地要求對象的年齡必須達到MaxTenuringThreshold才能晉升老年代,如果Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。

private static final int _1MB = 1024 * 1024;  
/**  
 * VM參數:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 
 * -XX:+PrintTenuringDistribution  
 */  
@SuppressWarnings("unused")  
public static void testTenuringThreshold2() { 
  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];  
}

Heap  
def new generation   total 9216K, used 4178K      //年輕代
eden space 8192K,  51% used ≈ 4MB 
from space 1024K,   0% used  
to   space 1024K,   0% used  
tenured generation   total 10240K, used 4756K     //老年代 
the space 10240K,  46% used ≈ 4MB+256KB*2

  根據堆內存分布日誌分析,當要分配allocation4時,發現Eden空間不夠進行GC,由於-XX:MaxTenuringThreshold設置為15,且Survivor區是可以容納下allocation1和allocation2的,照理說這兩個對象應該是要進入Survivor區的,但這兩個對象對沒有進入Survivor而是直接進入了老年代,這就是因為allocation1和allocation2兩對象相加為512KB,達到了Survivor的一半,且它們同年齡,所以會直接進入老年代。Survivor區不夠存allocation3也直接進入老年代,最後在Eden上分配allocation4。

5、空間分配擔保

  關於分配擔保機制JDK6前後有一點差別。  

  JDK6之前,在發生Minor GC之前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間。

  如果檢查老年代最大可用的連續空間大於新生代所有對象總空間,那麽Minor GC可以確保是安全的。

  如果檢查不成立,則虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗。

    如果允許擔保失敗,那麽會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,這裏取平均值是因為在實際完成內存回收前無法明確知道有多少對象會存活下來,所以也存在一定風險。

      如果大於歷次平均大小,將嘗試著進行一次Minor GC,盡管這次Minor GC是有風險的,Minor GC若執行失敗也會進行執行一次Full GC,這樣的失敗繞的圈子是最大的;

      如果小於歷次平均大小,或者HandlePromotionFailure設置不允許冒險,那這時也要改為進行一次Full GC。

  對以上的步驟歸納一下,先看老年代的可用空間能否容下新生代的所有對象,不能的話看是否開啟了分配擔保機制,允許就先執行Minor GC,否則直接進行Full GC。大部分情況下還是會將HandlePromotionFailure開啟分配擔保,避免頻繁Full GC。

  JDK6後,HandlePromotionFailure不再影響到虛擬機的空間分配擔保策略,變為只要老年代的連續空間大於新生代對象總大小或歷次晉升的平均大小就會進行Minor GC,否則將進行Full GC。

參考鏈接:

  https://www.jianshu.com/p/fa3569127416

  https://segmentfault.com/a/1190000004606059

JVM理論:(二/1)內存分配策略