【Java 虛擬機器筆記】記憶體分配策略相關整理

weixin_33766168發表於2019-02-27

文前說明

作為碼農中的一員,需要不斷的學習,我工作之餘將一些分析總結和學習筆記寫成部落格與大家一起交流,也希望採用這種方式記錄自己的學習之旅。

本文僅供學習交流使用,侵權必刪。
不用於商業目的,轉載請註明出處。

1. 概述

  • Java 技術體系中所提倡的自動記憶體管理最終可以歸結為自動化地解決了兩個問題。
    • 給物件 分配 記憶體以及 回收 分配給物件的記憶體。

Minor GC

  • 從新生代空間(包括 Eden 和 Survivor 區域)回收記憶體被稱為 Minor GC。
    • 當虛擬機器無法為一個新的物件分配空間時會觸發 Minor GC,比如當 Eden 區滿了。
      • 分配率越高,越頻繁執行 Minor GC。
    • 記憶體池被填滿的時候,其中的內容全部會被複制,指標會從 0 開始跟蹤空閒記憶體。
      • Eden 和 Survivor 區進行了標記和複製操作,取代了經典的標記、掃描、壓縮、清理操作。
      • Eden 和 Survivor 區不存在記憶體碎片。
      • 寫指標總是停留在所使用記憶體池的頂部。
    • 執行 Minor GC 操作時,不會影響到永久代。
      • 從永久代到新生代的引用被當成 GC Roots。
      • 從新生代到永久代的引用在標記階段被直接忽略掉。

Major GC / Full GC

  • 從老年代空間回收記憶體被稱為 Major GC。
    • 出現 Major GC,經常會伴隨至少一次的 Minor GC(但非絕對,在 Parallel Scavenge 收集器的收集策略裡有直接進行 Major GC 的策略選擇)。
    • MajorGC 的速度一般會比 Minor GC 慢 10 倍以上。
  • Full GC 的觸發機制。
    • 呼叫 System.gc() ,系統建議執行 Full GC,但是不必然執行。
    • 老年代空間不足。
    • 方法區空間不足。
    • 通過 Minor GC 後進入老年代的平均大小大於老年代的可用記憶體。
    • 由 Eden 區、From Survivor 區向 To Survivor 區複製時,物件大小大於 To Survivor 可用記憶體,則把該物件轉存到老年代,且老年代的可用記憶體小於該物件大小。

HotSpot 虛擬機器 GC 的過程

  • 初始階段,新建立的物件被分配到 Eden 區,From/To Survivor 兩塊區域都為空。
5714666-f7ae7c8f22803d69.png
初始階段
  • 當 Eden 區滿 Minor GC 被觸發。
5714666-b0d497c71981ff56.png
Eden 區滿
  • 經過掃描與標記,存活的物件被複制到 S0(From Survivor)區,不存活的物件被回收。
5714666-5c8630788e4da942.png
使用標記-複製將物件複製到 S0
  • 再一次 Minor GC,Eden 區和 S0 中沒有引用的物件被回收,存活的物件被複制到 S1(To Survivor)區。
    • 在上次 Minor GC 過程中移動到 S0 中的物件再複製到 S1 後其年齡要加 1。
    • Eden 區 S0 區被清空,所有存活的資料都複製到了 S1 區,並且 S1 區存在著年齡不一樣的物件。
5714666-5160c1da642ff6fd.png
再一次 Minor GC
  • 再一次 MinorGC 則重複這個過程,Eden 區和 S1(To Survivor)區被清空。
5714666-4aabf46a0775f11f.png
再一次 MinorGC
  • 再經過幾次 Minor GC 之後,當存活物件的年齡達到一個閾值之後(可通過引數配置,這裡設定為 8),就會被從新生代 Promotion 到老年代。
5714666-702575bbd4adef00.png
從新生代 Promotion 到老年代
  • 隨著 Minor GC 一次又一次的進行,不斷會有新的物件被 Promote 到老年代。
5714666-d1e29d2690466294.png
不斷會有新的物件被 Promote 到老年代
  • 最終,Major GC 將會在老年代發生,老年代的空間將會被清除和壓縮。
5714666-172e6adba3ad1c39.png
Major GC 將會在老年代發生

2. 物件優先在 Eden 分配

  • 大多數情況下,物件在新生代 Eden 區中分配記憶體,但 Eden 區沒有足夠空間進行分配時,虛擬機器將發起一次 Minor GC。
  • 通過 -XX:PrintGCDetails 引數列印 GC 日誌。
  • 例如使用 Serial + Serial Old(UseSerialGC) 或者 ParNew + Serial Old(UseParNewGC)
/**
 * -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
 */
public class Test {

    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];
    }
}
//[GC (Allocation Failure) [DefNew: 7307K->375K(9216K), 0.0023729 secs] 7307K->6519K(19456K), 0.0024046 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
//Heap
// def new generation   total 9216K, used 4635K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
//  eden space 8192K,  52% used [0x00000000fec00000, 0x00000000ff029140, 0x00000000ff400000)
//  from space 1024K,  36% used [0x00000000ff500000, 0x00000000ff55dd40, 0x00000000ff600000)
//  to   space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
// tenured generation   total 10240K, used 6144K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
//   the space 10240K,  60% used [0x00000000ff600000, 0x00000000ffc00030, 0x00000000ffc00200, 0x0000000100000000)
// Metaspace       used 3357K, capacity 4494K, committed 4864K, reserved 1056768K
//  class space    used 325K, capacity 386K, committed 512K, reserved 1048576K
  • 上例程式碼中設定最大記憶體空間為 20M,新生代 10M,老年代 10M。
    • DefNew 表示為 Serial 收集器。
    • 建立 allocation1、allocation2、allocation3 物件定義為各 2M,一共 6M。allocation4 物件為 4M。
    • 建立 allocation4 物件所需記憶體相加已經大於 Eden 區空間,產生一次 Minor GC。
      • 將 allocation1、allocation2、allocation3 移至老年代,tenured generation total 10240K, used 6144K,老年代使用了 6M。
      • Eden 區空閒出來,再將 allocation4 放入新生代,def new generation total 9216K, used 4635K,新生代使用了 4M。

3. 大物件直接進入老年代

  • 大物件是指需要大量連續記憶體空間的 Java 物件,最典型的大物件就是很長的字串以及陣列。
    • 大物件對虛擬機器記憶體分配來說是一個壞訊息,經常出現大物件容易導致記憶體還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來 " 安置 " 它們。
    • 虛擬機器提供了一個 -XX:PretenureSizeThreshold 引數來設定大物件的界限,大於此值則直接分配至老年代。
  • 例如使用 Serial + Serial Old(UseSerialGC) 或者 ParNew + Serial Old(UseParNewGC)
/**
 * -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
 */
public class Test {

    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {

        byte[] allocation1;
        allocation1 = new byte[4 * _1MB];
    }
}
//Heap
// def new generation   total 9216K, used 5423K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
//  eden space 8192K,  66% used [0x00000000fec00000, 0x00000000ff14bf48, 0x00000000ff400000)
//  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
//  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
// tenured generation   total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
//   the space 10240K,   0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
// Metaspace       used 3340K, capacity 4494K, committed 4864K, reserved 1056768K
//  class space    used 322K, capacity 386K, committed 512K, reserved 1048576K

//-XX:PretenureSizeThreshold=3145728
//Heap
// def new generation   total 9216K, used 1327K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
//  eden space 8192K,  16% used [0x00000000fec00000, 0x00000000fed4bf38, 0x00000000ff400000)
//  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
//  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
// tenured generation   total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
//   the space 10240K,  40% used [0x00000000ff600000, 0x00000000ffa00010, 0x00000000ffa00200, 0x0000000100000000)
// Metaspace       used 3333K, capacity 4494K, committed 4864K, reserved 1056768K
//  class space    used 319K, capacity 386K, committed 512K, reserved 1048576K
  • 上例程式碼中設定最大記憶體空間為 20M,新生代 10M,老年代 10M。
    • 未設定 -XX:PretenureSizeThreshold=3145728 前,def new generation total 9216K, used 1327K,記憶體使用了新生代空間。tenured generation total 10240K, used 0K,老年代為 0K。
    • 設定 -XX:PretenureSizeThreshold=3145728 後,tenured generation total 10240K, used 4096K,記憶體佔用直接分配到了老年代空間。

4. 長期存活的物件將進入老年代

  • Minor 的主要物件是新生代,物件在 Minor 後並不都會直接進入老年代,除非 Survivor 空間不夠,否則此存活物件會經過多次 Minor GC 後還生存的話才進入老年代,而虛擬機器預設的 Minor GC 次數為 15 次,可通過 -XX:MaxTenuringThreshold 進行次數設定。
  • 例如使用 JDK 1.6 環境,Serial + Serial Old(UseSerialGC) 或者 ParNew + Serial Old(UseParNewGC)
/**
 * -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
 */
public class Test {

    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {

        byte[] allocation1, allocation2, allocation3;

        allocation1 = new byte[1 * _1MB / 4];
        allocation2 = new byte[4 * _1MB];
        allocation3 = new byte[4 * _1MB];// 第一次 Minor GC
        allocation3 = null;
        allocation3 = new byte[4 * _1MB];// 第二次 Minor GC
    }
}
//-XX:MaxTenuringThreshold=15
//[GC [DefNew: 5463K->456K(9216K), 0.0053475 secs] 5463K->4552K(19456K), 0.0053895 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
//[GC [DefNew: 4964K->456K(9216K), 0.0010450 secs] 9060K->4552K(19456K), 0.0010711 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
//Heap
// def new generation   total 9216K, used 4775K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
//  eden space 8192K,  52% used [0x00000000f9a00000, 0x00000000f9e37be0, 0x00000000fa200000)
//  from space 1024K,  44% used [0x00000000fa200000, 0x00000000fa272170, 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 3497K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
//   the space 21248K,  16% used [0x00000000fae00000, 0x00000000fb16ab58, 0x00000000fb16ac00, 0x00000000fc2c0000)

//-XX:MaxTenuringThreshold=1
//[GC [DefNew: 5463K->458K(9216K), 0.0038139 secs] 5463K->4554K(19456K), 0.0038559 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
//[GC [DefNew: 4967K->0K(9216K), 0.0007399 secs] 9063K->4554K(19456K), 0.0007592 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
//Heap
// def new generation   total 9216K, used 4234K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
//  eden space 8192K,  51% used [0x00000000f9a00000, 0x00000000f9e227f0, 0x00000000fa200000)
//  from space 1024K,   0% used [0x00000000fa200000, 0x00000000fa2001e0, 0x00000000fa300000)
//  to   space 1024K,   0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
// tenured generation   total 10240K, used 4554K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
//   the space 10240K,  44% used [0x00000000fa400000, 0x00000000fa8729a8, 0x00000000fa872a00, 0x00000000fae00000)
// compacting perm gen  total 21248K, used 3598K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
//   the space 21248K,  16% used [0x00000000fae00000, 0x00000000fb1840b0, 0x00000000fb184200, 0x00000000fc2c0000)
  • 上例程式碼中設定最大記憶體空間為 20M,新生代 10M,老年代 10M。
    • 設定 -XX:MaxTenuringThreshold=15,必須經過 15 次 Minor GC 才能晉升到老年代,因此經過兩次 Minor GC 後,新生代仍有 4775K,即 allocation1 物件的仍在 From Survivor 區域。
    • 設定 -XX:MaxTenuringThreshold=1,第二次 Minor GC 時,新生代已經清空,allocation1 物件因為年齡為 1 進入老年代。

5. 動態物件年齡判定

  • 為了能更好地適應不同程式的記憶體狀況,虛擬機器並不是永遠地要求物件的年齡必須達到了 MaxTenuringThreshold 才能晉升老年代,如果 在 Survivor 空間中相同年齡所有物件大小的總和大於 Survivor 空間的一半,年齡大於或者等於該年齡的物件直接可以進入老年代,無須等到 MaxTenuringThreshold 中要求的年齡。
  • 例如使用 JDK 1.6 環境,Serial + Serial Old(UseSerialGC) 或者 ParNew + Serial Old(UseParNewGC)
/**
 * -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
 */
public class Test {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {

        byte[] allocation1, allocation2, allocation3, allocation4;

        allocation1 = new byte[1 * _1MB / 4];
        allocation2 = new byte[2 * _1MB / 4];//註釋前後對比(不註釋,則 allocation1 + allocation2 大於 Survivor 空間的一半)
        allocation3 = new byte[4 * _1MB];
        allocation4 = new byte[4 * _1MB];// 第一次 Minor GC
        allocation4 = new byte[4 * _1MB];// 第二次 Minor GC
    }
}
//allocation2 被註釋的情況下
//[GC [DefNew: 5463K->456K(9216K), 0.0040297 secs] 5463K->4552K(19456K), 0.0040705 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
//[GC [DefNew: 4964K->456K(9216K), 0.0035523 secs] 9060K->8648K(19456K), 0.0035859 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
//Heap
// def new generation   total 9216K, used 4690K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
//  eden space 8192K,  51% used [0x00000000f9a00000, 0x00000000f9e227e8, 0x00000000fa200000)
//  from space 1024K,  44% used [0x00000000fa200000, 0x00000000fa2721b8, 0x00000000fa300000)
//  to   space 1024K,   0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
// tenured generation   total 10240K, used 8192K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
//   the space 10240K,  80% used [0x00000000fa400000, 0x00000000fac00020, 0x00000000fac00200, 0x00000000fae00000)
// compacting perm gen  total 21248K, used 3507K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
//   the space 21248K,  16% used [0x00000000fae00000, 0x00000000fb16ce88, 0x00000000fb16d000, 0x00000000fc2c0000)

//allocation2 沒有註釋的情況下
//[GC [DefNew: 5975K->968K(9216K), 0.0042617 secs] 5975K->5064K(19456K), 0.0043078 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
//[GC [DefNew: 5476K->0K(9216K), 0.0030553 secs] 9572K->9160K(19456K), 0.0030796 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
//Heap
// def new generation   total 9216K, used 4234K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
//  eden space 8192K,  51% used [0x00000000f9a00000, 0x00000000f9e22750, 0x00000000fa200000)
//  from space 1024K,   0% used [0x00000000fa200000, 0x00000000fa200330, 0x00000000fa300000)
//  to   space 1024K,   0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
// tenured generation   total 10240K, used 9159K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
//   the space 10240K,  89% used [0x00000000fa400000, 0x00000000facf1f20, 0x00000000facf2000, 0x00000000fae00000)
// compacting perm gen  total 21248K, used 3524K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
//   the space 21248K,  16% used [0x00000000fae00000, 0x00000000fb171940, 0x00000000fb171a00, 0x00000000fc2c0000)
  • 上例程式碼中設定最大記憶體空間為 20M,新生代 10M,老年代 10M。
    • 當只有 allocation1 佔據 Survivor 時,還不到一半空間,所以還停留在 Survivor 空間。
    • 當 allocation2 也存在時,執行第一次 Minor GC 的時候 allocation1 和 allocation2 同時被移動到 Survivor 區域,但是因為 allocation1 和 allocation2 的總和已經達到了 Survivor 的一半,所以立刻被移動到老年代。

6. 空間分配擔保

  • 在 JDK 1.6 Update 24 之前,可以通過 HandlePromotionFailure 設定是否允許擔保失敗。
    • 如果允許,那麼會檢查老年代最大可用的連續空間是否大於歷次晉升到老年代物件的平均大小,如果大於,將嘗試再進行一次 Minor GC,儘管這次 Minor GC 是有風險的(當出現大量物件在 Minor GC 後仍然存活的情況(最極端的情況就是記憶體回收後新生代中所有物件都存活),就需要老年代進行分配擔保,把 Survivor 無法容納的物件直接進入老年代)。
    • 如果小於或者 HandlePromotionFailure 設定不允許冒險,那麼改為進行一次 Full GC。
  • JDK 1.6 Update 24 之後,在發生 Minor GC 之前,虛擬機器會先檢查老年代的連續空間大小是否大於新生代物件總大小或者歷次晉升的平均大小,如果是則進行 Minor GC,否則將進行 Full GC。
  • Parallel Scavenge 收集器與其他收集器在空間分配擔保上有一點差別,正常是在 Minor GC 前進行檢查, 而 Parallel Scavenge 收集器在 Minor GC 後也會進行檢查。
  • 例如使用 JDK 1.6 環境 Serial + Serial Old(UseSerialGC) 或者 ParNew + Serial Old(UseParNewGC)
/**
 * -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
 */
public class Test {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {

        byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7;

        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        // Minor GC allocation1、allocation2 和 allocation3 進入老年代
        allocation4 = new byte[2 * _1MB];// 第一次 Minor GC
        allocation5 = new byte[2 * _1MB];
        allocation6 = new byte[2 * _1MB];
        allocation1 = null;
        allocation2 = null;
        allocation3 = null;
        allocation7 = new byte[2 * _1MB];// 第二次 Minor GC
    }
}
//[GC [DefNew: 7255K->202K(9216K), 0.0047298 secs] 7255K->6346K(19456K), 0.0047709 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
//[GC [DefNew: 6849K->6849K(9216K), 0.0000174 secs][Tenured: 6144K->6346K(10240K), 0.0046928 secs] 12993K->6346K(19456K), [Perm : 3568K->3568K(21248K)], 0.0047640 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
//Heap
// def new generation   total 9216K, used 2185K [0x00000000f9a00000, 0x00000000fa400000, 0x00000000fa400000)
//  eden space 8192K,  26% used [0x00000000f9a00000, 0x00000000f9c224e8, 0x00000000fa200000)
//  from space 1024K,   0% used [0x00000000fa300000, 0x00000000fa300000, 0x00000000fa400000)
//  to   space 1024K,   0% used [0x00000000fa200000, 0x00000000fa200000, 0x00000000fa300000)
// tenured generation   total 10240K, used 6346K [0x00000000fa400000, 0x00000000fae00000, 0x00000000fae00000)
//   the space 10240K,  61% used [0x00000000fa400000, 0x00000000faa32b98, 0x00000000faa32c00, 0x00000000fae00000)
// compacting perm gen  total 21248K, used 3598K [0x00000000fae00000, 0x00000000fc2c0000, 0x0000000100000000)
//   the space 21248K,  16% used [0x00000000fae00000, 0x00000000fb183b78, 0x00000000fb183c00, 0x00000000fc2c0000)
  • 上例程式碼中設定最大記憶體空間為 20M,新生代 10M,老年代 10M。
    • allocation1、allocation2、allocation3 佔用 Eden 區域的 6M 空間。
    • allocation4 分配記憶體時,Eden 區域空間不足,觸發第一次 Minor GC。
    • Minor GC 結束後,allocation1、allocation2 和 allocation3 進入老年代,此時老年代剩餘空間為 4M。
    • allocation7 分配記憶體時,此時新生代已經放入 allocation4、allocation5 和 allocation6。
    • 無法放入 allocation7 即將觸發第二次 Minor GC。
    • 觸發前進行檢查,此時老年代剩餘空間為不足 4M,新生代物件總大小為 6M,歷次晉升的平均大小為 6M,因此改為進行一次 Full GC。
    • Full GC 後,新生代物件全部晉升,老年代因為 allocation1、allocation2、allocation3 被賦空,記憶體得到回收,因為晉升的 allocation4、allocation5 和 allocation6 ,記憶體大小還是 6M 左右。
    • 堆總量由 12M 降低至 6M。

參考資料

https://blog.csdn.net/weixin_39788856/article/details/80388002
https://www.cnblogs.com/wcd144140/p/5649553.html
https://blog.csdn.net/v123411739/article/details/78941793

相關文章