窺探JVM記憶體分配和回收的過程

zhongmingmao發表於2016-08-09

一、環境

JDK 垃圾收集器 是否啟用TLAB 通用JVM引數(堆記憶體分配見下圖)
1.6.0_65 Serial + Serial Old -Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8

圖片失效

二、說明

  • Minor GC
    • 發生在新生代,當Eden區域沒有足夠空間進行分配
    • Java物件大多具有短命的特性
    • Minor GC非常頻繁,速度也比較
  • Major GC / Full GC
    • 發生在老年代
    • 出現Major GC,經常伴隨至少一次Minor GC
    • SpeedOf (Minor GC) ≈ 10 * SpeedOf (Major GC)

三、示例

1. 物件優先分配在Eden區

1.1 說明

  • 新物件,優先考慮分配在Eden區域
  • 如果Eden區域沒有足夠的空間容納新物件,進行GC
    • 如果老年代有足夠的連續空間用來儲存所有新生代物件(或歷次晉升的平均大小) ⇒ Minor GC
      • 如果物件太大,以至於Survivor區域無法容納,物件直接晉升到老年代
      • 否則使用複製演算法,複製到Survivor區域
    • 否則 ⇒ 先進行一次Minor GC,若仍不滿足上述條件,進行Full GC
      • Full GC後依然記憶體不足,

1.2 程式碼

# 程式碼
public class TestAllocation {
    private static final int _1MB = 1024 * 1024;

    // JVM Args:
    // -XX:+PrintGCDetails
    // -XX:+UseSerialGC
    // -Xms20m -Xmx20m
    // -Xmn10m -XX:SurvivorRatio=8
    public static void main(String[] args) throws InterruptedException {
        System.out.println("===1. start full gc start===");
        System.gc();
        System.out.println("===1. start full gc end===\n");

        System.out.println("===2. gc logs===");
        byte[] a1 = new byte[_1MB / 4];
        byte[] a2 = new byte[4 * _1MB];
        byte[] a3 = new byte[4 * _1MB];// 一次Minor GC
        byte[] a4 = new byte[4 * _1MB];// 一次Minor GC
        byte[] a5 = new byte[4 * _1MB];// 一次Minor GC + 一次Full GC ⇒ OOM
    }
}

1.3 執行結果

# 僅保留關鍵資訊

===1. start full gc start===
[Full GC (System) [Tenured: 0K->426K(10240K)] 1974K->426K(19456K)
===1. start full gc end===

===2. gc logs===
[GC [DefNew: 4843K->256K(9216K)] 5270K->4778K(19456K)]
[GC [DefNew: 4352K->256K(9216K)] 8874K->8874K(19456K)]
[GC [DefNew: 4436K->4436K(9216K)][Tenured: 8618K->8618K(10240K)] 13055K->12970K(19456K)
[Full GC [Tenured: 8618K->8549K(10240K)] 12970K->12902K(19456K)

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

Heap
 def new generation   total 9216K, used 4657K
  eden space 8192K,  56% used
  from space 1024K,   0% used
  to   space 1024K,   0% used
 tenured generation   total 10240K, used 8549K
   the space 10240K,  83% used

1.4 執行示意圖

圖片失效

1.5 執行過程解析

  1. System.gc() ⇒ Full GC
    • 新生代已分配記憶體:0K
    • 老年代已分配記憶體:426K
  2. byte[] a1 = new byte[_1MB / 4];
    • Eden空間充足,為a1分配256K記憶體
  3. byte[] a2 = new byte[4 * _1MB];
    • Eden空間充足,為a2分配4096K記憶體
  4. byte[] a3 = new byte[4 * _1MB];
    • Eden空間不足,不能為a3分配4096K記憶體,需要進行GC
      • 4.1 判定是否需要Full GC
        • 老年代未分配的連續空間:10240-427 = 9813K
        • 所有的新生代物件:sizeof(a1)+size(a2)=256+4096 = 4352K
        • 9813 ≫ 4352 ⇒ Minor GC
      • 4.2 進行Minor GC
        • 1024 > 256 ⇒ remainingSpaceOf(Survivor) > sizeof(a1) ⇒ a1複製到to(Survivor區)
        • 4096 > 1024 ⇒ sizeof(a2) > sizeof(Survivor) ⇒ a2晉升到Tenured區
      • 4.3 Minor GC之後,Eden空間充足,為a3分配4096K記憶體
        • 新生代 – 4352K
          • Eden:4096K(a3)
          • from(Survivor):256K(a1)
          • to(Survivor):0K
        • 老年代 – 4522K
          • Tenured:426K + 4096K(a2)
  5. byte[] a4 = new byte[4 * _1MB];
    • Eden空間不足,不能為a4分配4096K記憶體,需要進行GC
      • 5.1 判定是否需要Full GC
        • 老年代未分配的連續空間:10240-427-4096 = 5717K
        • 所有的新生代物件:sizeof(a1)+size(a2)=256+4096 = 4352K
        • 5717 ≫ 4352 ⇒ Minor GC
      • 5.2 進行Minor GC
        • 第二次GC ⇒ a1由from複製到to
        • 4096 > 1024 ⇒ sizeof(a3) > sizeof(Survivor) ⇒ a3晉升到Tenured區
      • 5.3 Minor GC之後,Eden空間充足,為a4分配4096K記憶體
        • 新生代 – 4436K
          • Eden:84K + 4096K(a4)
          • from(Survivor):0K
          • to(Survivor):256K(a1)
        • 老年代 – 8618K
          • Tenured:426K + 4096K(a2) + 4096K(a3)
  6. byte[] a5 = new byte[4 * _1MB];
    • Eden空間不足,不能為a5分配4096K記憶體,需要進行GC
      • 6.1 判定是否需要Full GC
        • 老年代未分配的連續空間:10240-427-4096-4096 = 1621K
        • 所有的新生代物件:sizeof(a1)+size(a2)=256+4096 = 4352K
        • 1621 ≪ 4352 ⇒ Minor GC + Full GC
      • 6.2 進行Minor GC
        • 第三次GC ⇒ a1由to複製到from
        • 4096 > 1621 ⇒ sizeof(a4) > remainingSpaceOf(Tenured) ⇒ a4無法晉升到Tenured區
      • 6.3 Minor GC之後,堆空間記憶體佈局沒有變化,Eden空間依舊不足,進行Full GC
        • 老年代減少了69K,忽略不計,依然保留著a2a3
        • 新生代增加了17k,忽略不計,依然保留著a1a4
      • 6.4 Full GC之後,新生代和老年代記憶體都不足以為a5分配記憶體,丟擲OOM異常

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

2.1 說明

  • 大物件:需要大量連續記憶體空間的Java物件,如長字串大陣列
    • 程式碼中儘量避免使用短命大物件
  • -XX:PretenureSizeThreshold=?(Byte)
    • 新物件的大小大於PretenureSizeThreshold,直接分配在老年代
    • 作用
      • 新生代採用的是複製演算法,避免Eden和Survivor之間的記憶體複製
      • 降低GC頻率

2.2 程式碼

public class TestPretenureSizeThreshold {
    private static final int _1MB = 1024 * 1024;

    // JVM Args
    // -XX:+PrintGCDetails
    // -XX:+UseSerialGC
    // -Xms20m -Xmx20m
    // -Xmn10m -XX:SurvivorRatio=8
    // -XX:PretenureSizeThreshold=3145728
    public static void main(String[] args) {
        byte[] a1 = new byte[4 * _1MB]; // 直接分配到tenured
        byte[] a2 = new byte[4 * _1MB]; // 直接分配到tenured,無需GC
    }
}

2.3 執行結果

# 僅保留關鍵資訊

Heap
 def new generation   total 9216K, used 2302K
  eden space 8192K,  28% used
  from space 1024K,   0% used
  to   space 1024K,   0% used
 tenured generation   total 10240K, used 8192K
   the space 10240K,  80% used

2.4 執行過程解析

  • sizeof(a1) > PretenureSizeThreshold ⇒ 直接分配在Tenured
  • sizeof(a2) > PretenureSizeThreshold ⇒ 直接分配在Tenured
    • 如果不設定PretenureSizeThreshold,在分配a2前會產生一次Minor GC,最終a1Tenureda2Eden

3. 長期存活的物件晉升到老年代

3.1 說明

  • 物件年齡:物件每經歷一次Minor GC,物件年齡加1
  • JVM引數:-XX:MaxTenuringThreshold=?,預設是15

3.2 程式碼

public class TestTenuringThreshold {
    private static final int _1MB = 1024 * 1024;
    private static final int MAX_OBJ_AGE = 2;

    // JVM Args
    // -XX:+PrintGCDetails
    // -XX:+UseSerialGC
    // -Xms20m -Xmx20m
    // -Xmn10m -XX:SurvivorRatio=8
    // -XX:MaxTenuringThreshold=3
    public static void main(String[] args) {
        System.out.println("===1. start full gc start===");
        System.gc();
        System.out.println("===1. start full gc end===\n");

        System.out.println("===2. gc logs===");
        byte[] a1 = new byte[_1MB / 4];
        byte[] a2 = new byte[4 * _1MB];
        byte[] a3 = null;
        for (int i = 0; i < MAX_OBJ_AGE; ++i) {
            // 執行一次, a1的年齡加1 , 最終a1的年齡為MAX_OBJ_AGE
            // 如果 MAX_OBJ_AGE > MaxTenuringThreshold ⇒ a1 晉升到 Tenured
            // 如果 MAX_OBJ_AGE ≦ MaxTenuringThreshold ⇒ a1 停留在 Survivor
            a3 = null;
            a3 = new byte[4 * _1MB];
        }
    }
}

3.3 執行結果

# 僅保留關鍵資訊

# MAX_OBJ_AGE = 3時
===1. start full gc start===
[Full GC (System) [Tenured: 0K->430K(10240K)] 1974K->430K(19456K)
===1. start full gc end===

===2. gc logs===
[GC [DefNew: 4679K->256K(9216K)] 5110K->4783K(19456K)]
[GC [DefNew: 4352K->256K(9216K)] 8879K->4783K(19456K)]
[GC [DefNew: 4352K->256K(9216K)] 8879K->4783K(19456K)]
Heap
 def new generation   total 9216K, used 4516K
  eden space 8192K,  52% used
  from space 1024K,  25% used
  to   space 1024K,   0% used
 tenured generation   total 10240K, used 4526K
   the space 10240K,  44% used

# MAX_OBJ_AGE = 4時
===1. start full gc start===
[Full GC (System) [Tenured: 0K->426K(10240K)] 1974K->426K(19456K)
===1. start full gc end===

===2. gc logs===
[GC [DefNew: 4843K->256K(9216K)] 5270K->4779K(19456K)]
[GC [DefNew: 4437K->256K(9216K)] 8960K->4779K(19456K)]
[GC [DefNew: 4409K->257K(9216K)] 8931K->4780K(19456K)]
[GC [DefNew: 4391K->0K(9216K)] 8913K->4780K(19456K)]
Heap
 def new generation   total 9216K, used 4449K
  eden space 8192K,  54% used
  from space 1024K,   0% used
  to   space 1024K,   0% used
 tenured generation   total 10240K, used 4779K
   the space 10240K,  46% used

3.4 執行過程解析

  • a1a2現在Eden分配記憶體
  • 第一次為a3分配記憶體時,必須進行Minor GCa2被晉升Tenureda3分配在Edena1被複制到from(Survivor),此時a1的物件年齡為1
    • 隨後每次迴圈中,先將a3置為nulla3原先引用的記憶體變成了垃圾,後續可回收
    • 再次為a3分配記憶體,此時Eden區記憶體不足,進行Minor GC
      • 目前存活的物件僅僅是a1,將其複製到to(另一塊Survivor),a1的物件年齡加1
      • fromEden區清空,並在Eden為a3分配記憶體
  • MAX_OBJ_AGE = 3  MaxTenuringThreshold時
    • from space 1024K, 25% used ⇒ a1依舊停留在Survivor
  • MAX_OBJ_AGE = 4 > MaxTenuringThreshold時
    • [GC [DefNew: 4391K->0K(9216K)] 8913K->4780K(19456K)] ⇒ a1晉升到Tenured
    • from space 1024K, 0% used ⇒ Survivor已經無存活物件

4. 動態物件年齡判定

4.1 說明

  • 在Survivor中相同年齡(物件年齡 ≧ 2)的所有物件大小之和 ≧ 0.5 * sizeof(Survivor) ⇒ 大於或等於該年齡的物件直接晉升到老年代,無須考慮MaxTenuringThreshold

4.2 程式碼一(單個1/2物件)

public class TestDynamicObjectAge {
    private static final int _1MB = 1024 * 1024;
    private static final int MAX_OBJ_AGE = 1;

    // JVM Args
    // -XX:+PrintGCDetails
    // -XX:+UseSerialGC
    // -Xms20m -Xmx20m
    // -Xmn10m -XX:SurvivorRatio=8
    // -XX:MaxTenuringThreshold=3
    public static void main(String[] args) {
        int al;
        System.out.println("===1. start full gc start===");
        System.gc();
        System.out.println("===1. start full gc end===\n");

        System.out.println("===2. gc logs===");
        byte[] a1 = new byte[_1MB / 2]; # 單個1/2物件
        byte[] a2 = new byte[4 * _1MB];
        byte[] a3 = null;
        for (int i = 0; i < MAX_OBJ_AGE; ++i) {
            // 相同年齡(物件年齡 ≧ 2)的所有物件大小之和 ≧ 0.5 * sizeof(Survivor)
            // ⇒ 大於或等於該年齡的物件直接晉升到老年代,無需考慮MaxTenuringThreshold
            a3 = null;
            a3 = new byte[4 * _1MB];
        }
    }
}

4.3 程式碼一執行結果

# 僅保留關鍵資訊

# MAX_OBJ_AGE = 1時
===1. start full gc start===
[Full GC (System) [Tenured: 0K->429K(10240K)] 1974K->429K(19456K)
===1. start full gc end===

===2. gc logs===
[GC [DefNew: 5099K->513K(9216K)] 5529K->5039K(19456K)]
Heap
 def new generation   total 9216K, used 4773K
  eden space 8192K,  52% used
  from space 1024K,  50% used # a1的物件年齡為1,依舊停留在Survivor區域
  to   space 1024K,   0% used
 tenured generation   total 10240K, used 4525K
   the space 10240K,  44% used

# MAX_OBJ_AGE = 2時
===1. start full gc start===
[Full GC (System) [Tenured: 0K->426K(10240K)] 1974K->426K(19456K)
===1. start full gc end===

===2. gc logs===
[GC [DefNew: 5099K->513K(9216K)] 5526K->5036K(19456K)]
[GC [DefNew: 4694K->1K(9216K)] 9216K->5037K(19456K)] 
# a1的物件年齡為2 < MaxTenuringThreshold,且a2等於0.5 * sizeof(Survivor),直接晉升到老年代
Heap
 def new generation   total 9216K, used 4317K
  eden space 8192K,  52% used
  from space 1024K,   0% used 
  # a1的物件年齡為2 < MaxTenuringThreshold,且a2等於0.5 * sizeof(Survivor),直接晉升到老年代
  to   space 1024K,   0% used
 tenured generation   total 10240K, used 5036K
   the space 10240K,  49% used

4.4 程式碼二(四個1/8物件)

public class TestDynamicObjectAge {
    private static final int _1MB = 1024 * 1024;
    private static final int MAX_OBJ_AGE = 1;

    // JVM Args
    // -XX:+PrintGCDetails
    // -XX:+UseSerialGC
    // -Xms20m -Xmx20m
    // -Xmn10m -XX:SurvivorRatio=8
    // -XX:MaxTenuringThreshold=3
    public static void main(String[] args) {
        int al;
        System.out.println("===1. start full gc start===");
        System.gc();
        System.out.println("===1. start full gc end===\n");

        System.out.println("===2. gc logs===");
        byte[] a1_1 = new byte[_1MB / 8]; # 四個1/8物件
        byte[] a1_2 = new byte[_1MB / 8];
        byte[] a1_3 = new byte[_1MB / 8];
        byte[] a1_4 = new byte[_1MB / 8];
        byte[] a2 = new byte[4 * _1MB];
        byte[] a3 = null;
        for (int i = 0; i < MAX_OBJ_AGE; ++i) {
            // 相同年齡(物件年齡 ≧ 2)的所有物件大小之和 ≧ 0.5 * sizeof(Survivor)
            // ⇒ 大於或等於該年齡的物件直接晉升到老年代,無需考慮MaxTenuringThreshold
            a3 = null;
            a3 = new byte[4 * _1MB];
        }
    }
}

4.5 程式碼二執行結果

# 僅保留關鍵資訊

# MAX_OBJ_AGE = 1時
===1. start full gc start===
[Full GC (System) [Tenured: 0K->427K(10240K)] 1974K->427K(19456K)
===1. start full gc end===

===2. gc logs===
[GC [DefNew: 4971K->514K(9216K)] 5399K->5038K(19456K)]
Heap
 def new generation   total 9216K, used 4859K
  eden space 8192K,  53% used # a1_1、a1_2、a1_3、a1_4的物件年齡為1,依舊停留在Survivor區域
  from space 1024K,  50% used
  to   space 1024K,   0% used
 tenured generation   total 10240K, used 4523K
   the space 10240K,  44% used

# MAX_OBJ_AGE = 2時
===1. start full gc start===
[Full GC (System) [Tenured: 0K->427K(10240K)] 1974K->427K(19456K)
===1. start full gc end===

===2. gc logs===
[GC [DefNew: 4971K->514K(9216K)] 5399K->5037K(19456K)]
[GC [DefNew: 4694K->0K(9216K)] 9218K->5037K(19456K)]
# a1_1、a1_2、a1_3和a1_4的物件年齡為2 < MaxTenuringThreshold,且它們之和等於0.5 * sizeof(Survivor),晉升到老年代
Heap
 def new generation   total 9216K, used 4316K
  eden space 8192K,  52% used
  from space 1024K,   0% used
  # a1_1、a1_2、a1_3和a1_4的物件年齡為2 < MaxTenuringThreshold,且它們之和等於0.5 * sizeof(Survivor),晉升到老年代
  to   space 1024K,   0% used
 tenured generation   total 10240K, used 5037K
   the space 10240K,  49% used
 compacting perm gen  total 21248K, used 4965K
   the space 21248K,  23% used

三、參考資料

相關文章