JVM垃圾回收机制(4)--内存分配和回收策略

前言

Github:https://github.com/yihonglei/jdk-source-code-reading(java-jvm)

JVM内存结构

JVM类加载机制

JVM内存溢出分析

HotSpot对象创建、内存、访问

JVM垃圾回收机制(1)--如何判定对象可以回收

JVM垃圾回收机制(2)--垃圾收集算法

JVM垃圾回收机制(3)--垃圾收集器

JVM垃圾回收机制(4)--内存分配和回收策略

一 内存分配与回收策略概述

    对象的内存分配往大方向讲,就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地栈上分配),

对象主要分配在新生代的Eden区上,如果启用了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下

也可能直接分配在老年代中,分配的规则并不是百分百固定的,分配细节取决于垃圾收集器组合,还有虚拟机中

与内存相关的参数设置。

HotSpot分代收集内存图: 

JVM垃圾回收机制(4)--内存分配和回收策略_第1张图片

Virtual:为动态收缩区域,当垃圾回收获得高吞吐量或低停顿后,会尽量缩小区域内存,以获得更好的空间局部性。 

以下内容实例分析基于JVM server模式下验证,因为用的是jdk8 64位默认就是server模式,不能切换到client模式,

就不换32 jdk,来回切换JVM的client和server模式了,不过要特别注意Client模式和Server模式分配策略有些不同。

二 对象优先在Eden区分配

    对象通常在新生代Eden区中分配,当Eden区没有足够空间分配时,虚拟机将发生一次Minor GC。

与Minor GC对应的是Major GC、Full GC。

新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,

所以Minor GC非常频繁,一般回收速度也比较快。

老年代GC(Major GC/Full GC):指发生在老年代的GC,出现Full GC,经常会伴随至少一次的Minor GC(

但非绝对的,在Parallel Scavenge收集器的可以配置Full GC收集策略)。Full GC的速度一般会比Minor GC

慢10倍以上。Major GC和Full GC概念上目前没有明确定义,并不是简单的Major GC负责清理老年代,

Full GC清理堆空间新生代和老年代,可以参考这里的解释,从GC停顿做出了解释。

https://dzone.com/articles/minor-gc-vs-major-gc-vs-full,如果访问不了,翻下墙。

1、eg1

相关参数:

-verbose:gc 输出显示虚拟机运行信息;

-XX:+PrintGCDetails 打印内存回收日志;

-Xmx20M -Xms20M -Xmn10M 限制堆大小为20M,不可以扩展,10M分配给新生代,剩下10分配给老年代;

-XX:SurvivorRatio=8 配置Eden区与一个Survivor区的比例,这里是默认的8:1,不用显示配置也可以;

代码示例:

package com.jpeony.jvm.gc;

/**
 * 优先分配在eden演示!
 *
 * VM参数: -verbose:gc -XX:+PrintGCDetails -Xmx20M -Xms20M -Xmn10M -XX:SurvivorRatio=8
 *
 * 参数说明:
 * -verbose:gc 输出显示虚拟机运行信息;
 *
 * -XX:+PrintGCDetails 打印内存回收日志;
 *
 * -Xmx20M -Xms20M -Xmn10M 限制堆大小为20M,不可以扩展,10M分配给新生代,剩下10分配给老年代;
 *
 * -XX:SurvivorRatio=8 配置Eden区与一个Survivor区的比例,这里是默认的8:1,不用显示配置也可以;
 *
 * @author yihongeli
 */
public class AllocationTest01 {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        byte[] allocation = new byte[4 * _1MB];
    }
}

运行结果:

Heap
 PSYoungGen      total 9216K, used 6414K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 78% used [0x00000000ff600000,0x00000000ffc43b58,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 3235K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 353K, capacity 388K, committed 512K, reserved 1048576K

    从GC日志可以看到,新生代eden区占用78%,from、to两个Survivor区和ParOldGen老年代都没有使用,

验证了对象优先在Eden区分配的事实。

    按理说,eden区分配只是4M,应该占50%才对,事实分配后占用的比实际分配的要多,主要是因为Java对象

并不是一个人在战斗,其它部分也占用了内存。

2、eg2

把上面代码实现修改如下:

package com.jpeony.jvm.gc;

/**
 * VM参数: -verbose:gc -XX:+PrintGCDetails -Xmx20M -Xms20M -Xmn10M -XX:SurvivorRatio=8
 *
 * 参数说明:
 * -verbose:gc 输出显示虚拟机运行信息;
 *
 * -XX:+PrintGCDetails 打印内存回收日志
 *
 * -Xmx20M -Xms20M -Xmn10M 限制堆大小为20M,不可以扩展,10M分配给新生代,剩下10分配给老年代
 *
 * -XX:SurvivorRatio=8 配置Eden区与一个Survivor区的比例,这里是默认的8:1,不用显示配置也可以;
 *
 * @author yihongeli
 */
public class AllocationTest02 {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        byte[] allocation1 = new byte[2 * _1MB];
        byte[] allocation2 = new byte[2 * _1MB];
        byte[] allocation3 = new byte[5 * _1MB];
    }
}

运行结果:

Heap
 PSYoungGen      total 9216K, used 6579K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 80% used [0x00000000ff600000,0x00000000ffc6ccb8,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 10240K, used 5120K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 50% used [0x00000000fec00000,0x00000000ff100010,0x00000000ff600000)
 Metaspace       used 3333K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 359K, capacity 388K, committed 512K, reserved 1048576K

 程序执行后,eden区用了80%,from 未占用,to未占用,而ParOldGen老年代占用50%。

当allocation1,allocation2分配的时候,eden区占用80%,咱们分配4M,实际占用了6.4M,

这个时候再分配allocation3是5M,eden区剩余内存不够5M,然后allocation3直接分配在老年代,

因为分配的内存>=Eden大小的一半,就直接放入了老年代。

三 大对象直接进入老年代

    大对象是指需要大量连续内存空间的Java对象,最经典的对象就是那种很长的字符串以及数组。

大对象对虚拟机内存分配来说就是一个坏消息,经常出现大对象容易导致内存还有不少空间时就

提前触发垃圾收集以获取足够的连续空间来存储大对象。

    虚拟机提供了-XX:PretenureSizeThreadshold参数来设置大对象的阈值,超过阈值的对象直接分配到老年代。

这样做的目的是为了避免在Eden区和两个Survivor区之间发生大量的内存复制。

1、eg1

相关参数:

-verbose:gc 输出显示虚拟机运行信息;

-XX:+PrintGCDetails 打印内存回收日志;

-Xmx20M -Xms20M -Xmn10M 限制堆大小为20M,不可以扩展,10M分配给新生代,剩下10分配给老年代;

-XX:SurvivorRatio=8 配置Eden区与一个Survivor区的比例,这里是默认的8:1,不用显示配置也可以;

-XX:PretenureSizeThreshold=3145728 (3M)设置大对象的阀值,大于该值,直接进入老年代;

-XX:+UseSerialGC 指定收集器为Serial

注意:PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效,我这里用的是Jdk8,

默认使用Parallel Scavenge,一般不需要设置,如果非得需要,可以考虑ParNew和CMS收集器组合。

代码示例:

package com.jpeony.jvm.gc;

/**
 * VM参数:
 * -verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728 -XX:+UseSerialGC
 *
 * 参数说明:
 * -verbose:gc 输出显示虚拟机运行信息;
 *
 * -XX:+PrintGCDetails 打印内存回收日志;
 *
 * -Xms20M -Xmx20M -Xmn10M 限制堆大小为20M,不可以扩展,10M分配给新生代,剩下10分配给老年代;
 *
 * -XX:SurvivorRatio=8 配置Eden区与一个Survivor区的比例,这里是默认的8:1,不用显示配置也可以;
 *
 * -XX:PretenureSizeThreshold=3145728 (3M)设置大对象的阀值,大于该值,直接进入老年代;【lll】
 *
 * -XX:+UseSerialGC 指定收集器为Serial
 *
 * 注意:PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效,我这里用的是Jdk8,
 * 默认使用Parallel Scavenge,一般不需要设置,如果非得需要,可以考虑ParNew和CMS收集器组合。
 *
 * 程序说明:
 * 从结果可以看出eden区没怎么使用,from、to两个Survivor去未使用,老年代用了40%。
 * 因为分配对象为4M,大于设置的3M阀值,直接在老年代进行分配。
 *
 * @author yihongeli
 */
public class PretenureSizeThreshold {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        byte[] allocation = new byte[4 * _1MB];
    }
}

运行结果:

Heap

 def new generation   total 9216K, used 2319K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)

  eden space 8192K,  28% used [0x00000000fec00000, 0x00000000fee43d48, 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 3235K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 353K, capacity 388K, committed 512K, reserved 1048576K

从结果可以看出eden区没怎么使用,from、to两个Survivor去未使用,老年代用了40%。

因为分配对象为4M,大于设置的3M阀值,直接在老年代进行分配。

四 长期存活的对象将进入老年代

    虚拟机为每一个对象定义了一个对象年龄(Age)计数器。如果对象在Eden区出生经过第一次Minor GC

后仍然存活,并能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设置为1。对存活在

Survivor中的对象,每"熬过"一次Minor GC,年龄就增加1,当它的年龄增加到一定年龄阀值(默认15岁),

就将会被晋升 到老年代中。对老年代年龄阀值可以通过-XX:MaxTenuringThreshold设置。

相关参数:

-verbose:gc 输出显示虚拟机运行信息;

-XX:+PrintGCDetails 打印内存回收日志;

-Xms20M -Xmx20M -Xmn10M 限制堆大小为20M,不可以扩展,10M分配给新生代,剩下10分配给老年代;

-XX:SurvivorRatio=8 配置Eden区与一个Survivor区的比例,这里是默认的8:1,不用显示配置也可以;

-XX:MaxTenuringThreshold=1 年龄阀值设置,默认为15岁。

代码示例:

package com.jpeony.jvm.gc;

/**
 * VM参数:
 * -verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
 *
 * 参数说明:
 * -verbose:gc 输出显示虚拟机运行信息;
 *
 * -XX:+PrintGCDetails 打印内存回收日志;
 *
 * -Xms20M -Xmx20M -Xmn10M 限制堆大小为20M,不可以扩展,10M分配给新生代,剩下10分配给老年代;
 *
 * -XX:SurvivorRatio=8 配置Eden区与一个Survivor区的比例,这里是默认的8:1,不用显示配置也可以;
 *
 * -XX:MaxTenuringThreshold=1 年龄阀值设置,默认为15岁。
 *
 *
 * 程序说明:
 * 从执行结果可以看出eden使用50%,from、to两个Survivor区未使用,老年代使用50%。
 * 按照程序内存分配,从上到下分析,allocation1分配_1MB/4进入eden区,allocation2分配4MB进入eden区,
 * 当第一次分配allocation3时,发现eden区内存不够,直接触发Minor GC,allocation1和allocation2按理说都进入
 * Survivor区,但是Survivor区只有1MB,只能容得下allocation1,allocation1进入Survivor区并且年龄为1,
 * 将在下一次GC时晋升到老年代,而allocation2通过担保机制直接进入老年代,allocation3分配4MB则在eden区。
 * 当第二次分配allocation3时,分配4M,这样eden区不够,因为上一次分配allocation3是4MB,要比实际大,
 * 现在又来4M,eden区只有8MB,当然eden区内存就不够了,这个时候又触发了一次Minor GC,allocation1年龄加1,
 * 晋升到老年代,allocation2也还在老年代,上一次的allocation3因为被设置为null,直接被清除,eden区变为8MB内存,
 * 第二次的allocation3被分配到eden区。
 * 最终结果就是:allocation1, allocation2分配在老年区,allocation3分配在新生代的eden区。
 *
 * @author yihongeli
 */
public class TenuringThresholdTest01 {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        byte[] allocation1, allocation2, allocation3;
        allocation1 = new byte[_1MB / 4];
        allocation2 = new byte[4 * _1MB];
        allocation3 = new byte[4 * _1MB];

        allocation3 = null;
        allocation3 = new byte[4 * _1MB];
    }
}

结果:

Heap
 def new generation   total 9216K, used 4150K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  50% used [0x00000000fec00000, 0x00000000ff00dbf8, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 5140K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  50% used [0x00000000ff600000, 0x00000000ffb05210, 0x00000000ffb05400, 0x0000000100000000)
 Metaspace       used 3345K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 361K, capacity 388K, committed 512K, reserved 1048576K

从执行结果可以看出eden使用50%,from、to两个Survivor区未使用,老年代使用50%。

按照程序内存分配,从上到下分析,allocation1分配_1MB/4进入eden区,allocation2分配4MB进入eden区,

当第一次分配allocation3时,发现eden区内存不够,直接触发Minor GC,allocation1和allocation2按理说都进入

Survivor区,但是Survivor区只有1MB,只能容得下allocation1,allocation1进入Survivor区并且年龄为1,

将在下一次GC时晋升到老年代,而allocation2通过担保机制直接进入老年代,allocation3分配4MB则在eden区。

当第二次分配allocation3时,分配4M,这样eden区不够,因为上一次分配allocation3是4MB,要比实际大,

现在又来4M,eden区只有8MB,当然eden区内存就不够了,这个时候又触发了一次Minor GC,allocation1年龄加1,

晋升到老年代,allocation2也还在老年代,上一次的allocation3因为被设置为null,直接被清除,eden区变为8MB内存,

第二次的allocation3被分配到eden区。

最终结果就是: allocation1, allocation2分配在老年区,allocation3分配在新生代的eden区。

五 动态对象年龄判断

    对象的年龄到达了MaxTenuringThreshold可以进入老年代,同时,如果在survivor区中相同年龄所有对象

大小的总和大于survivor区的一半,年龄大于等于该年龄的对象就可以直接进入老年代。

无需等到MaxTenuringThreshold中要求的年龄。

相关参数:

-verbose:gc 输出显示虚拟机运行信息;

-XX:+PrintGCDetails 打印内存回收日志;

-Xms20M -Xmx20M -Xmn10M 限制堆大小为20M,不可以扩展,10M分配给新生代,剩下10分配给老年代;

-XX:SurvivorRatio=8 配置Eden区与一个Survivor区的比例,这里是默认的8:1,不用显示配置也可以;

-XX:MaxTenuringThreshold=15 年龄阀值设置,默认为15岁。

代码示例:

package com.lanhuigu.jvm.gc;

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

    /**
     * VM参数: -verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
     * -XX:+UseSerialGC -XX:MaxTenuringThreshold=15 -XX:+PrintTenuringDistribution
     */
    public static void main(String[] args) {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[_1MB / 4];
        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 4235K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  51% used [0x00000000fec00000, 0x00000000ff022828, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400618, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 5371K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  52% used [0x00000000ff600000, 0x00000000ffb3ec10, 0x00000000ffb3ee00, 0x0000000100000000)
 Metaspace       used 3339K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 360K, capacity 388K, committed 512K, reserved 1048576K

从结果可以看出eden使用51%,from、to两个Survivor未使用,老年代使用了52%。

allocation1、allocation2、allocation3分配进入Eden区。

当第一次给allocation4分配内存时,eden区内存不够,发生一次Minor GC,

此时allocation1、allocation2将会进入survivor区,而allocation3通过担保机制将会进入老年代。

第二次发生在给allocation4分配内存时,此时,survivor区的allocation1、allocation2达到了survivor区容量的一半,

将会进入老年代,此次GC可以清理出allocation4原来的4MB空间,并将allocation4分配在Eden区。

最终,allocation1、allocation2、allocation3在老年代,allocation4在Eden区。

六 空间分配担保

    在发生Minor GC时,虚拟机会检查老年代连续的空闲区域是否大于新生代所有对象的总和,

若成立,则说明Minor GC是安全的,否则,虚拟机需要查看HandlePromotionFailure的值,看是否运行担保失败,

若允许,则虚拟机继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,

若大于,将尝试进行一次Minor GC;

若小于或者HandlePromotionFailure设置不运行冒险,那么此时将改成一次Full GC,

以上是JDK Update 24之前的策略,之后的策略改变了,只要老年代的连续空间大于新生代对象总大小

或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。冒险是指经过一次Minor GC后有大量对象存活,

而新生代的survivor区很小,放不下这些大量存活的对象,所以需要老年代进行分配担保,

把survivor区无法容纳的对象直接进入老年代。

代码示例:

package com.jpeony.jvm.gc;

/**
 * VM参数:
 * -verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+HandlePromotionFailure
 *
 * 程序说明:
 * 一开始allocation1、allocation2、allocation3分配在eden区,同时allocation1被置为无效。
 *
 * 当第一次给allocation4分配内存时,eden区内存不够,发生一次Minor GC,
 *
 * 由于老年代的连续可用空间大于存活的对象总和,所以allocation2、allocation3将会进入老年代,
 *
 * allocation1的空间将被回收,此时整个eden区被清空,又变为8MB,有空间让allocation4分配在eden区;
 *
 * 接下来allocation5、allocation6接着往eden区分配,同时allocation4、allocation5、allocation6被置为无效。
 *
 * 当第二次给allocation7分配内存时,eden区内存不够,发生一次Minor GC,allocation4、allocation5、
 *
 * allocation6所占的内存全部回收,把整个eden区清空,变为8MB,然后将allocation7分配在新生代eden区。
 *
 * 最后,allocation2、allocation3在老年代,allocation7在新生代。
 *
 * @author yihongeli
 */
public class HandlePromotion {
    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        byte[] allocation1, allocation2, allocation3, allocation4,
                allocation5, allocation6, allocation7, allocation8;
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation1 = null;
        allocation4 = new byte[2 * _1MB];
        allocation5 = new byte[2 * _1MB];
        allocation6 = new byte[2 * _1MB];
        allocation4 = null;
        allocation5 = null;
        allocation6 = null;
        allocation7 = new byte[2 * _1MB];
    }
}

运行结果:

Heap
 def new generation   total 9216K, used 2130K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  26% used [0x00000000fec00000, 0x00000000fee14930, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 4611K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  45% used [0x00000000ff600000, 0x00000000ffa80d58, 0x00000000ffa80e00, 0x0000000100000000)
 Metaspace       used 2568K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 275K, capacity 386K, committed 512K, reserved 1048576K

一开始allocation1、allocation2、allocation3分配在eden区,同时allocation1被置为无效。

当第一次给allocation4分配内存时,eden区内存不够,发生一次Minor GC,

由于老年代的连续可用空间大于存活的对象总和,所以allocation2、allocation3将会进入老年代,

allocation1的空间将被回收,此时整个eden区被清空,又变为8MB,有空间让allocation4分配在eden区;

接下来allocation5、allocation6接着往eden区分配,同时allocation4、allocation5、allocation6被置为无效。

当第二次给allocation7分配内存时,eden区内存不够,发生一次Minor GC,allocation4、allocation5、

allocation6所占的内存全部回收,把整个eden区清空,变为8MB,然后将allocation7分配在新生代eden区。

最后,allocation2、allocation3在老年代,allocation7在新生代。

参考文献

《深入理解Java虚拟机》 (第二版) 周志明 著;

你可能感兴趣的:(#,---JVM,Thinking,In,JVM)