java内存分配

来源:互联网 发布:百度云连接网络失败bug 编辑:程序博客网 时间:2024/05/09 15:17

java对内存的回收和收集器的主要思想我在前一篇博客进行了详细的描述,这里主要讲的是java如何实现对内存的管理的,在讲解之前我们需要做的是理解如何配置jvm参数和参数的意义,下面我也会提到一些参数的作用和使用的场合,并会达到什么效果,但前提是必须了解jvm中堆内存是如何分配的,也可以看我之前的一篇文章:

http://blog.csdn.net/maodoubi/article/details/47981693

以下的部分资料也可以看看周志明老师写的《深入理解java虚拟机》这本书,对java虚拟机进行了详细的介绍

1、对象优先在Eden区进行分配

大多数情况下,对象都是在新生代Eden区分配,当Eden区没有足够的空间进行分配时,虚拟机就会发生一次MinorGC(新生代垃圾回收),所谓MinorGC就是在给对象分配内存时,发现此时Eden区已经没有任何空间能够容纳这个对象,就会把Eden区中存活的对象放入Survivor区,但是如果Survivor区不能容纳这些存活的对象,就只能通过分配担保机制提前转移到老年代去,旧生代用于存放新生代中经过多次垃圾回收 (也即Minor GC) 仍然存活的对象,下面是一个图示可以非常完美的说明,Form Space和To Space分别就是两个Survivor区:


可以使用一段代码来验证当新生代的内存不足以容纳一个对象时,就会发生一次Minor GC,也就是将新生代中存活的对象放入到老年代中,写代码之前我们需要配置一下jvm参数,方法网上有很多,比较流行的方法是右键类选Run->Run as Configurations,选择Arguments,在vm arguments中写入:
-Xloggc:E:\\gc.log -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseParNewGC
解释一下每句的含义:
-Xloggc:E:\\gc.log:生成日志文件写入E盘的gc.log文件里
-Xms20M -Xmx20M -Xmn10M:设置初始堆大小为20M,不扩展,新生代堆为10M
-XX:+PrintGCDetails:打印一份详细日志
-XX:SurvivorRatio=8:设置Eden区域可Survivor区域的比例为8:1
-XX:+UseParNewGC:这里使用ParNew和Serial Old的组合
然后设计代码如下:
public class Test {    //设计一个byte数组长度,大小为1024*1024,其实就是1MB    private static final int _1MB=1024*1024;    public static void testAllocation(){                 byte[] allocation1,allocation2,allocation3,allocation4;        //为四个byte数组分别分配2M,2M,2M,4M的空间        allocation1=new byte[2*_1MB];        allocation2=new byte[2*_1MB];        allocation3=new byte[2*_1MB];        /*        当为第四个数组分配内存时已经没有多余的内存可以容纳下这个数组了         * 所以就会在新生代Eden区域进行一次垃圾回收,但此时Survivor区域也不能容纳         * 就会将上面的三个数组全部放入到老年代中进行担保         * 这就是所谓的进行一次MinorGC(新生代垃圾回收)        */         allocation4=new byte[4*_1MB];    }     public static void main(String[] args) {         Test.testAllocation();     }}

看一下日志的结果:


从上面的日志中可以清楚的看到总的PSYoungGen(新生代)的空间大小为一个Eden区域加上一个Survivor区域的和,新生代eden区占用了4M多的内存,因为eden区可能本身就被占用了部分内存,但是老年代中有60%的内存,也就是6M,说明刚开始的三个变量共6M的内存都被放入到了老年代中,而新生代只容纳了最后一个变量4M的内存,说明发生了一次MinorGC将新生代中存活的对象都被放入到了老年代中。

2、大对象直接进入老年代:

所谓的大对象就是那种占据大量连续空间的对象,例如长字符串或者大数组,这种对象会占据极大的内存空间,经常出现大对象可能会导致内存还有不少空间是就提前出发垃圾收集以获取足够的连续空间来安置他们,这对于我们的程序肯定是极为不利的,如何避免大对象,jvm的一个做法就是通过设置-XX:PretenureSizeThreshold参数来实现对大对象的处理,当我们的对象超过这个值时,jvm会直接将这个对象放入到老年代中,这可以避免在新生代中利用复制算法进行大量的复制,因为新生代区是用复制算法实现的,下面的例子可以说明对象过大是将对象直接放入到了老年代:
首先jvm参数中加上-XXPretenureSizeThreshold=3145728,设置最大允许的对象大小为3M,这里不能直接写3M
代码实现:
public class Test {    //设计一个byte数组长度,大小为1024*1024,其实就是1MB    private static final int _1MB=1024*1024;    public static void testAllocation(){                 byte[] allocation1;        /*         * 直接放入一个4M字节的数组,这个时候由于超过了-XX:PretenureSizeThreshold指定的         * 范围3M,这个参数只能使用字节的形式表示,这个时候会直接将这个4M数组放入到老年代中         */        allocation1=new byte[4*_1MB];    }     public static void main(String[] args) {         Test.testAllocation();     }}
打印日志内容:


从上面的日志中可以看到当直接放入一个4M的数据时,由于超过了PretenureSizeThreshold参数3M的限制,而被直接放到老年代中去了,因为老年代中刚好也增加了4M的内存

3、长期存活的对象将直接被放入到老年代:

我们知道垃圾收集器回收对象是通过分代收集的思想实现的,那么内存必须知道将哪些对象应该放入老年代,哪些对象放入新生代,jvm给出了一中对象年龄的计数器,上面提到到eden区域没有足够的内存空间存放我们定义的对象时,就会进行一次MinorGC,但是如果Survivor区域可以容纳的话,就会被移动到Survivor区域,并将对象的年龄设置成1,对象没熬过一次MinorGC,年龄就会加1,当年龄超过一个阈值就会被放入老年代中,我们可以通过-XX:MaxTenuringThreshold=10来设置阈值,通过-XX:+PrintTenuringDistribution可以打印出比较清楚的日志,可以看出阈值增加时的内存变化,下面是一个实例,分别是阈值为1和阈值为10的情况:

public class Test {    //设计一个byte数组长度,大小为1024*1024,其实就是1MB    private static final int _1MB=1024*1024;    public static void testAllocation() throws InterruptedException{        System.gc();//做一次垃圾清理,保证Survivor区域空间不会超过1/2        Thread.sleep(500);//休眠500ms,保证清理已经结束了        @SuppressWarnings("unused")        byte[] allocation1,allocation2,allocation3,allocation5;        allocation1=new byte[_1MB/4];        allocation2=new byte[4*_1MB];        allocation3=new byte[4*_1MB];        allocation3=null;        allocation3=new byte[4*_1MB];    }     public static void main(String[] args) throws InterruptedException {         Test.testAllocation();     }}
阈值为1时的情况,可以看到当进行第一次垃圾回收时survivor区域被放入了256K的内存,但是第二次回收时survivor空间变为了0K,并与阈值为10的情况相比,老年代还多了3%的内存,初略计算刚好是256KB左右,说明新生代survivor区域的内容被放入到了老年代中:


阈值为10时的情况,可以看到在发生两次收集时survivor区域的内存始终是256KB:



4、动态年龄对象判断:

jvm其实不是一定要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,如果Survivor空间中相同年龄所有对象的大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就会直接进入到老年代,而不需要等到MaxTenuringThreshold中要求的年龄,比如上方的实例中可以看到,我们在做操作时进行了一次垃圾清理,可以尽量保证survivor中的内存被清理,否则可能会出现survivor区域的内存大于1/2而导致直接将survivor中相同年龄的对象放入老年代,也就不会出现想要的结果,例如在上方的代码allocation1后面allocation5分配一个256kb的空间,最后得到结果如下所示:


结果显示当插入512KB的内存时,此时survivor中年龄相等的有allocation1和allocation5,并且他们之和等于survivor区域1/2的大小,所以即使没有等到MaxTenuringThreshold中要求的年龄,这两个对象仍然被放置到了老年代中进行担保,因为老年代中的内存刚好比上面一个阈值为10的实例增加了5%左右的内存,大约就是512KB左右的内存空间。

5、空间分配担保:

在发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代剩余空间的大小,如果大于,就改为直接进行一次FullGC(或者叫Major GC ,也就是老年代垃圾收集),如果小于就会查看HandlePeomotionFailure设置是否允许担保失败,如果允许,那只会进行Minor GC;如果不允许,则也要改为进行一次FullGC。当出现大量Minor GC对象仍然存活的情况下,并且Survivor无法容纳这些对象的时候,就需要让老年代进行担保,但前提是老年代有足够的空间来容纳,但是一共有多少对象存活下来是不能明确知道的,所以只好取之前每一次回收晋升到老年代的对象容量的平均值,与老年代的剩余空间进行比较,决定是否进行FullGC来让老年代腾出更多空间,当我们打HandlePeomotionFailure开关打开时,可以避免进行过多的FullGC,毕竟FullGC消耗的时间几乎是MinorGC的十倍左右。

0 0
原创粉丝点击