JVM 内存分配与回收策略

内存分配与回收策略

1.概述

JAVA 体系中提倡的自动内存管理最终归根结底为自动化的解决了两个问题:给对象分配内存和回收分配给
对象的内存。

2.对象优先在Eden区分配

大多数情况下,对象在新生代Eden区中分配,当Eden区没有足够的空间分配时,虚拟机将发起一次Minor GC。

public class MemoryAllocation {
    private static int _1MB = 1024 * 1024;

    /**
     * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M 
     * -XX:SurvivorRatio=8 -XX:+PrintGCDetails
     * @param args
     */
    public static void main(String[] args) {
        byte[] allocation1 = new byte[3 * _1MB];
        byte[] allocation2 = new byte[3 * _1MB];
    }
}

在这里插入图片描述

由上图可以看出堆主要有三大区域,新生代、老年代和元数据区,新对象优先分配到新生代的Eden区了
  • Minor GC干了那些事?
    由于现在虚拟机都采用分代收集算法,利用不同代对象生命周期的特点使用不同的收集算法达到更高的回收内存效率,新生代对象的特点就是朝生暮死(方法调用产生的临时对象等),所以采用复制算法(只需要复制少数存活的对象到Survivor区达到内存的快速回收)回收内存。
public class MemoryAllocation {
    private static int _1MB = 1024 * 1024;
    private static int _500KB = 512 * 1024;

    /**
     * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M 
     * -XX:SurvivorRatio=8 -XX:+PrintGCDetails
     * @param args
     */
    public static void main(String[] args) {
        byte[] allocation1 = new byte[3 * _1MB];
        byte[] allocation2 = new byte[3 * _1MB];
        allocation1 = null; 
        allocation2 = null;
        // 下面语句会触发MinorGC,回收掉allocationh1和allocation2占用的空间,
        //将Eden区存活的对象复制到Survivor区,并给allocatio3对象分配内存
        byte[] allocation3 = new byte[_500KB]; 
    }
}

在这里插入图片描述

可以看出虚拟机触发了一次Minor GC,新生代使用内存从7784K变成了680K,并将Eden区的存活的信息复制
到Survivor区,这次GC后Eden区有足够的空间给allocation3分配,所以最后的Eden区内存使用了1274K。
  • 如果Minor GC过程中存活的对象比较多,在Survivor区放不下,会发生什么?
public class MemoryAllocation {
    private static int _1MB = 1024 * 1024;
    private static int _500KB = 512 * 1024;

    /**
     * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails
     * @param args
     */
    public static void main(String[] args) {
        byte[] allocation1 = new byte[3 * _1MB];
        byte[] allocation2 = new byte[3 * _1MB];
        allocation1 = null;
        // 下面语句会触发MinorGC,回收掉allocationh1占用的空间,但由于allocation2对象还存活,Survivor区只有1024K,放不下allocation2对象,使用空间分配担保机制直接在老年代给allocation2分配空间
        byte[] allocation3 = new byte[_500KB];
    }
}

在这里插入图片描述
可以看出新生代内存使用从7784K变成了728K,总内存内存使用情况从7784K变成3808K(老年代3080K+新生代728K)就证明回收了allocation1的内存,并且由于Survivor区放不下allocation2对象,所以直接通过空间分配担保机制将allocation2对象放在了老年代,最后再给allocation3在新生代的Eden区分配内存。

3.大对象直接进入老年代

大对象是指需要大量连续内存空间的java对象,最典型的大对象就是那种很长的字符串以及数组。经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。

public class BigObject {
    private static final int _1MB = 1024 * 1024;
    /**
     * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M 
     * -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728
     * @param args
     */
    public static void main(String[] args) {
        byte[] bigObject = new byte[8 * _1MB];
    }
}

在这里插入图片描述

可以看出,没有发生Minor GC,from和to都是0%,直接在老年代分配空间。

4.长期存活的对象进入老年代

现在虚拟机都是用分代收集思想来管理内存,所以内存回收时就必须能识别哪些对象放在新生代,哪些放在老年代,虚拟机通过给每个对象定义一个年龄计数器来实现,当年龄达到某个值时(默认15),就将会晋升到老年代中。通过虚拟机参数-XX:MaxTenuringThreshold指定

public class DynamicObject {
    private static final int _1MB = 1024 * 1024;
    /**
     * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M 
     * -XX:+PrintGCDetails -XX:SurvivorRatio=8 
     * -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
     * @param args
     */
    public static void main(String[] args) {
        byte[] allocation1 = new byte[_1MB / 2];
        byte[] allocation2 = new byte[3 * _1MB];
        allocation2 = null;
        //发生第一次Minor GC,allocation1进入Survivor区age变为1,
        //allocation2的内存区域被回收,给allocation3分配内存空间
        byte[] allocation3 = new byte[3 * _1MB];
        allocation3 = null;
        //byte[] allocation4 = new byte[_1MB / 2];
        //byte[] allocation5 = new byte[_1MB / 2];
        byte[] allocation6 = new byte[3 * _1MB];
        allocation6 = null;
        //发生第二次minor GC,allocation3和allocation6的内存空间被回收,allocation1的
        //age变为2,此时-XX:MaxTenuringThreshold=1,所以allocation1进入老年代
        byte[] allocation7 = new byte[3 * _1MB];
    }
}

在这里插入图片描述
可以看到发生了两次Minor GC,第一次Eden区已经被占用了5224K的内存,当要给allocation3分配内存的时候,发现Eden区可用内存不够,发生第一次Minor GC,使得年轻代内存从5224K变为1000K,总内存从5224K变成1168K,allocation1进入Survivor区并且age变为1,第一次Minor GC结束。现在Eden区有可用空间,然后分别给allocation3对象和allocation6对象分配3MB的内存,两个3MB+第一次Minor GC后的1000K=现在Eden区被使用了7300K,然后要给allocation7分配内存时发现不够,JVM再次执行Minor GC,allocation1对象依然存活,但age变为了2>-XX:MaxTenuringThreshold=1,所以会直接进入老年代

5.动态对象年龄判断

虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

public class DynamicObject {
    private static final int _1MB = 1024 * 1024;
    /**
     * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M 
     * -XX:+PrintGCDetails -XX:SurvivorRatio=8 
     * -XX:MaxTenuringThreshold=3 -XX:+PrintTenuringDistribution
     * @param args
     */
    public static void main(String[] args) {
        byte[] allocation1 = new byte[_1MB / 4 + 50];
        byte[] allocation2 = new byte[_1MB / 4 + 50];
        byte[] allocation3 = new byte[4 * _1MB];
        allocation3 = null;
        //发生第一次Minor GC,allocation1和allocation2进入Survivor区age变为1,
        //allocation2的内存区域被回收,给allocation3分配内存空间
        byte[] allocation4 = new byte[3 * _1MB];
        allocation4 = null;
        //byte[] allocation5 = new byte[_1MB / 2];
        byte[] allocation6 = new byte[3 * _1MB];
        allocation6 = null;
        //发生第二次minor GC,allocation3和allocation6的内存空间被回收,
        //allocation1的age变为2,此时-XX:MaxTenuringThreshold=1,所以allocation1进入老年代
        byte[] allocation7 = new byte[3 * _1MB];
        allocation7 = null;
        allocation6 = new byte[3 * _1MB];
        allocation6 = null;
        allocation4 = new byte[3 * _1MB];
    }
}

在这里插入图片描述

这个我测试了好几次都没有成功,有成功的或者知道问题出在哪的麻烦告我一声

6.空间分配担保机制

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果大于,则说明这次Minor GC是确保安全的。JDK6 Update24之后的规则变为只要老年代的连续空间大于新生代对象总和或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。

public class SpaceGuarantee {
    private static final int _1MB = 1024 * 1024;
    /**
     * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
     * @param args
     */
    public static void main(String[] args) {
        byte[] allocation1 = new byte[3 * _1MB];
        allocation1 = null;
        byte[] allocation2 = new byte[3 * _1MB];
        //第一次Minor GC,allocation2进入老年代,现在老年代被占用了3MB
        byte[] allocation3 = new byte[3 * _1MB];
        allocation3 = null;
        byte[] allocation4 = new byte[3 * _1MB];
        //第二次Minor GC,allocation4进入老年代,现在老年代被占用了6MB
        byte[] allocation5 = new byte[3 * _1MB];
        allocation5 = null;
        byte[] allocation6 = new byte[4 * _1MB];
        //下面这行注释掉结果会不同
        allocation4 = null;
        byte[] allocation7 = new byte[3 * _1MB];
    }
}

未注释掉allocation4 = null;这行代码的结果
在这里插入图片描述
可以看出发生了3次Minor GC和一次Full GC,未注释掉allocation4=null这行代码结果,前两次触发Minor GC是老年代连续空间大于新生代所有对象总和,第三次Minor GC是由于满足老年代连续空间大于历次晋升的平均大小(第一次晋升3MB + 第二次晋升3MB)。从第三次执行结果来看,新生代内存没有被回收掉(还是812K),当执行Minor GC过程中,需要将allocation6对象放到Survivor区,发现不够,就给老年代放,还是不够。这时就会触发一次Full GC。这次Full GC就会回收掉老年代allocation4对象占用老年代的3MB空间并给allocation6在老年代分配空间,新生代的空间被全部回收。
注释掉allocation4 = null;这行代码的结果
在这里插入图片描述
前三次Minor GC和上面一样,唯一不同的是最后一次Full GC,老年代空间依然被allocation2和allocation4占用未能回收内存,但新生代allocation5可以回收,所以可以给allocation7在新生代分配空间了。

public class SpaceGuarantee {
    private static final int _1MB = 1024 * 1024;
    /**
     * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
     * @param args
     */
    public static void main(String[] args) {
        byte[] allocation1 = new byte[3 * _1MB];
        allocation1 = null;
        byte[] allocation2 = new byte[3 * _1MB];
        //第一次Minor GC,allocation2进入老年代,现在老年代被占用了3MB
        byte[] allocation3 = new byte[3 * _1MB];
        allocation3 = null;
        byte[] allocation4 = new byte[4 * _1MB];
        //第二次Minor GC,allocation4进入老年代,现在老年代被占用了7MB
        byte[] allocation5 = new byte[3 * _1MB];
        allocation5 = null;
        byte[] allocation6 = new byte[2 * _1MB];
        //下面这行注释掉结果会不同
        allocation4 = null;
        //第三次Minor GC,allocation
        byte[] allocation7 = new byte[3 * _1MB];
    }
}

在这里插入图片描述
前两次触发Minor GC是由于老年代连续空间大于历次晋升的平均大小,第三次发生Full GC是由于经过前两次Minor GC老年代已经使用了7MB(allocation2 + allocation4),现在只剩不到3MB的空间,而现在新生代对象5MB(allocation5 + allocation6),所以不满足老年代连续空间大于新生代对象总和,另一个条件由于第一次进入老年代是3MB,第二次是4MB,所以这两次晋升的平均大小大于老年代连续空间的总和,也不满足,这种情况下,虚拟机会触发Full GC。可以看出回收掉了新生代的所有空间,老年代由于allocation2和allocation4存活,所以未能回收。当在新生代给allocation5和allocation6分配了空间后,已经占用了5412K,再给allocation7分配空间时Eden区已经分配不下,由于空间分配担保机制的两个条件不满足,所以触发Full GC,这次Full GC将回收掉老年代allocation4占用的4MB空间,然后在老年代给allocation6分配2MB的空间,可以看到最后一次Full GC后,老年代占用5734K(allocation2 + allocation6),而新生代allocation5在老年代分配空间,allocation6当做垃圾被回收,所以新生代的空间被全部回收。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值