Java虛擬機內存分配與回收策略

Java虛擬機中的內存分配與回收策略就是 Java的自動內存管理,其最核心的部分就是內存中對象的分配與回收。所以在了解虛擬機內存分配與回收策略之前我們有必要了解一下Java堆內存的組成部分。

堆內存示意圖

從上圖可以得知,堆內存主要分為新生代、老年代、永久代幾部分組成,其中新生代又分為一個Eden區(qū)和兩個Survivor區(qū),其比例為8:1。JDK1.8之后,用元空間(Metaspace)的區(qū)域取代了堆中的永久代區(qū)域(永久代使用的是JVM的堆內存空間,而元空間使用的是物理內存,直接受到本機的物理內存限制)。


  • 對象優(yōu)先在Eden分配

大多數情況下,對象在新生代Eden區(qū)中分配。當Eden區(qū)中沒有足夠空間進行分配時 ,虛擬機將發(fā)起一次Minor GC。代碼測試如下:

/*虛擬機參數配置如下 : -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails  */
public class MemoryAllocate {

    public static void main(String[] args) {
       
    }
}

首先解釋下 虛擬機參數配置 : -Xms20M -Xmx20M -Xmn10M 三個參數限制了Java堆大小為20MB,不可擴展(-Xms設置堆容量的最小值,-Xmx設置堆容量的最大值),其中10M分配給新生代(-Xmn設置對容量新生代的大小),剩下的10M分配給老年代;-XX:SurvivorRatio=8 決定了新生代中Eden區(qū)與一個Survivor區(qū)的空間比例為8:1;-XX:+PrintGCDetails 打印內存回收日志。接下來看一下運行結果:

Heap
 PSYoungGen      total 9216K, used 2148K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 26% used [0x00000000ff600000,0x00000000ff819270,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 10240K, used 0K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff600000)
 Metaspace       used 3143K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 342K, capacity 388K, committed 512K, reserved 1048576K

從結果中分析得知新生代(PSYoungGen)總可用空間為Eden區(qū)與一個Survivor(from 與to其中)區(qū)的總和:(8192+1024)這里說的是新生代的可用空間,而不是總空間,總空間大小為Eden與兩個Survivor區(qū)的和;老年代(ParOldGen) 的空間大小為10240k;Metaspace(元空間,JDK1.8之后用于取代永久代的空間)。因此虛擬機參數配置已經生效,另外,雖然我們什么都沒做,Eden區(qū)的空間也已經被使用26%。接下來看看第二段代碼:

public class MemoryAllocate {

    public static void main(String[] args) {
        //連續(xù)向堆中申請5個1M的空間
        byte[] allocate1 = new byte[1 * 1024 * 1024];
        byte[] allocate2 = new byte[1 * 1024 * 1024];
        byte[] allocate3 = new byte[1 * 1024 * 1024];
        byte[] allocate4 = new byte[1 * 1024 * 1024];
        byte[] allocate5 = new byte[1 * 1024 * 1024];
    }
}

輸出結果如下 : 
Heap
 PSYoungGen      total 9216K, used 7598K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 92% used [0x00000000ff600000,0x00000000ffd6bab8,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 10240K, used 0K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff600000)
 Metaspace       used 3231K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 352K, capacity 388K, committed 512K, reserved 1048576K

PSYongGen的使用空間從2148k增加到7598k,而兩個Survivor區(qū)根本沒有使用,所以新增加的5450K空間全部在Eden中,證實了對象優(yōu)先在Eden區(qū)中分配的觀點。接下來看第三段代碼:

public class MemoryAllocate {

    public static void main(String[] args) {
        //連續(xù)向堆中申請5個1M的空間
        byte[] allocate1 = new byte[1 * 1024 * 1024];
        byte[] allocate2 = new byte[1 * 1024 * 1024];
        byte[] allocate3 = new byte[1 * 1024 * 1024];
        byte[] allocate4 = new byte[1 * 1024 * 1024];
        byte[] allocate5 = new byte[1 * 1024 * 1024];
        //再申請1一個1M的空間
        byte[] allocate6 = new byte[1 * 1024 * 1024];
    }
}

輸出結果如下:
[GC (Allocation Failure) [PSYoungGen: 7270K->968K(9216K)] 7270K->6096K(19456K), 0.0035282 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 968K->0K(9216K)] [ParOldGen: 5128K->5873K(10240K)] 6096K->5873K(19456K), [Metaspace: 3218K->3218K(1056768K)], 0.0060934 secs] [Times: user=0.08 sys=0.02, real=0.01 secs] 
Heap
 PSYoungGen      total 9216K, used 1353K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 16% used [0x00000000ff600000,0x00000000ff7527c8,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 5873K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 57% used [0x00000000fec00000,0x00000000ff1bc6f8,0x00000000ff600000)
 Metaspace       used 3232K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 352K, capacity 388K, committed 512K, reserved 1048576K

其中觸發(fā)了一次GC回收和一次Full GC:

  • [PSYoungGen: 7270K->968K(9216K)] 表示GC前年輕代占用內存7270K,GC后占用內存968K,內>存區(qū)域總容量9M;
  • 7270K->6096K(19456K) 表示GC前堆占用內存7270K,GC后占用內存6096K,堆總容量20M;
  • [Full GC (Ergonomics) [PSYoungGen: 968K->0K(9216K)] 表示進行了一次Full GC,后面會說到Full GC與GC的區(qū)別。
    接下來看結果:
    從結果中我們可以得知ParOldGen老年代中使用了5873K的內存,而eden區(qū)中的內存使用情況反而變小了。因為當給allocate6分配內存的時候,eden區(qū)中的的剩余空間已經不足分配allocate6所需的1M內存, 因此發(fā)生GC,而GC期間發(fā)現已有的5個1M大小的對象無法全部放入Survivor空間,所以只好通過擔保分配機制將這五個對象提前轉移到老年代中。

注:上面代碼運行可能會產生不一樣的結果,那就需要讀者另行分析了。

  • 大對象直接進入老年代

所謂的大對象主要指,需要大量連續(xù)內存空間的Java對象,最典型的例子就是那種很長的字符串以及數組。虛擬機提供了-XX:PretenureSizeThreshold參數,令大于這個設置值的對象直接在老年代分配。這樣做的好處是避免了在Eden區(qū)和兩個Survivor區(qū)之間發(fā)生大量的內存復制(新生代采用復制算法回收內存)。

/*-XX:PretenureSizeThreshold=3145728 虛擬機參數配置*/
public class MemoryAllocate {
    
    public static void main(String[] args) {
        //申請一個8M的空間
        byte[] allocate = new byte[8 * 1024 * 1024];
    }
}
輸出結果 :
Heap
 PSYoungGen      total 9216K, used 2478K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 30% used [0x00000000ff600000,0x00000000ff86b970,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 10240K, used 8192K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 80% used [0x00000000fec00000,0x00000000ff400010,0x00000000ff600000)
 Metaspace       used 3231K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 352K, capacity 388K, committed 512K, reserved 1048576K

很明顯的我們可以看到8M的空間全部分配到了老年代之中。這里有一點需要注意的PretenureSizeThreshold參數只對Serial和ParNew兩款收集器有效。本列中使用的Parallel Scavenge收集器,在所申請的空間大于eden區(qū)可使用的空間時,就會直接將大對象直接分配到老年代。

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

如果對象在Eden區(qū)出生并且經歷過一次Minor GC后仍然存活,并且能夠被Servivor容納,將被移動到Servivor空間中,并且把對象年齡設置成為1。對象在Servivor區(qū)中每熬過一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(默認15歲),就將會被晉級到老年代中。虛擬機提供了-XX:MaxTenuringThreshold參數來設置這個閾值。

  • 動態(tài)對象年齡判定

為了更好地適應不同程序的內存狀況,虛擬機并不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉級到老年代,如果在Servivor空間中相同年齡所有對象的大小總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入到老年代,無須登到MaxTenuringThreshold中要求的年齡

  • 空間分配擔保

在發(fā)生Minor GC 之前,虛擬機會檢查老年代最大可 用的連續(xù)空間是否大于新生代所有對象總空間,如果這個條件成立,那么Minor GC可以確保是安全的。如果不成立,則虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗。如果允許那么會繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于晉級到老年代對象的平均大小,如果大于,將嘗試進行一次Minor GC,盡管這次MinorGC 是有風險的:如果小于,或者HandlePromotionFailure設置不允許冒險,那這時也要改為進行一次Full GC

上面提到了Minor GC依然會有風險,是因為新生代采用復制收集算法,假如大量對象在Minor GC后仍然存活(最極端情況為內存回收后新生代中所有對象均存活),而Survivor空間是比較小的,這時就需要老年代進行分配擔保,把Survivor無法容納的對象放到老年代。老年代要進行空間分配擔保,前提是老年代得有足夠空間來容納這些對象,但一共有多少對象在內存回收后存活下來是不可預知的,因此只好取之前每次垃圾回收后晉升到老年代的對象大小的平均值作為參考。使用這個平均值與老年代剩余空間進行比較,來決定是否進行Full GC來讓老年代騰出更多空間。


  • Minor GC 和 Full GC
  1. 新生代GC(Minor GC):指發(fā)生新生代的垃圾收集動作,因為Java對象大多數都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快。
  2. 老年代GC(Major GC / Full GC): 指發(fā)生在老年代的GC,出現了Major GC,至少會伴隨一次的Minor GC(但非絕對的,在Parallel Scavenge收集器的收集策略里就有直接進行Major GC的策略選擇過程)。Major GC的速度一般會比Minor GC的速度慢10倍以上。

上一篇:Java虛擬機垃圾收集
下一篇:虛擬機類加載機制

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容