文档章节

窥探JVM内存分配和回收的过程

闪电
 闪电
发布于 2016/08/03 23:42
字数 2734
阅读 39
收藏 3

一、环境

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

三、参考资料


本文转载自:http://zhongmingmao.me/2016/08/01/memory_allocation.html

共有 人打赏支持
闪电
粉丝 74
博文 392
码字总数 6789
作品 0
海淀
技术主管
私信 提问
深入理解JVM——JVM性能调优实战 原

如何在高性能服务器上进行JVM调优? 为了充分利用高性能服务器的硬件资源,有两种JVM调优方案,它们都有各自的优缺点,需要根据具体的情况进行选择。 1、采用64位操作系统,并为JVM分配大内存...

Theriseof
2018/11/23
0
0
Java GC系列:Java垃圾回收详解

Java的内存分配与回收全部由JVM垃圾回收进程自动完成。与C语言不同,Java开发者不需要自己编写代码实现垃圾回收。这是Java深受大家欢迎的众多特性之一,能够帮助程序员更好地编写Java程序。 ...

满风
2015/04/10
0
0
JVM内存管理和JVM垃圾回收机制

你对JVM内存组成结构和JVM垃圾回收机制是否熟悉,这里和大家简单分享一下,希望对你的学习有所帮助,首先来看一下JVM内存结构,它是由堆、栈、本地方法栈、方法区等部分组成,结构图如下所示...

laigous
2014/05/13
0
0
Java 垃圾回收机制

什么是自动垃圾回收? 自动垃圾回收是一种在堆内存中找出哪些对象在被使用,还有哪些对象没被使用,并且将后者删掉的机制。所谓使用中的对象(已引用对象),指的是程序中有指针指向的对象;...

JavaSon712
2018/08/28
5.2K
19
Java虚拟机基础——4内存回收机制

Java虚拟机整体篇幅如下: Java虚拟机基础——1Java的内存模型 Java虚拟机基础——2JVM运行时数据区 Java虚拟机基础——3类加载机制 Java虚拟机基础——4内存回收机制 本篇文章的内容如下: ...

隔壁老李头
2018/10/03
0
0

没有更多内容

加载失败,请刷新页面

加载更多

取变量的地址赋值给另一个变量,C通过,C++编译出错

取变量的地址赋值给另一个变量,C通过。正常运行,C++编译出错。 代码如下: #include <stdio.h>int main(int argc, char *argv[]){int x = 3;int *p = &x;int y = p;/*c ...

SamXIAO
今天
1
0
利用隐写术实施攻击

尽管隐写术是一种低频攻击途径,但网络犯罪分子已经开始利用它结合社交媒体的普遍性和快速传播性来传递恶意有效负载。 低调但有效的隐写技术虽然是旧把戏,但将代码隐藏在看似正常的图像中,...

Linux就该这么学
今天
3
0
YII2的乐观锁和悲观锁

乐观锁与悲观锁¶ Web应用往往面临多用户环境,这种情况下的并发写入控制, 几乎成为每个开发人员都必须掌握的一项技能。 在并发环境下,有可能会出现脏读(Dirty Read)、不可重复读(Unrep...

echojson
今天
2
0
UCOS线程切换原理

黑客画家
今天
3
0
最牛Java架构师进阶路线(年薪80W)

1、源码分析专题 详细介绍源码中所用到的经典设计思想,看看大牛是如何写代码的,提升技术审美、提高核心竞争力。 帮助大家寻找分析源码的切入点,在思想上来一次巨大的升华。知其然,并知其...

别打我会飞
今天
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部