Java堆空间详解之新生代和老年代

Java运行时内存区主要分为 运行时栈(虚拟机栈)、本地方法栈、程序计数器、堆空间、方法区(JDK1.8之后是元空间),今天来聊一聊我们的堆空间.

一个对象或者数组的创建是在堆空间中完成的,堆的大小是有限的(固定的),所以,必不可少的我们要考虑一下堆的空间分配问题和对象的分配问题.

空间分配问题:

堆空间默认的初始化内存最小值为 系统内存/64,最大值为系统内存/4;我们可以通过命令 -Xms666m -Xmx666m,来将堆空间的初始化大小最小值和最大值都设置为666m;

public class HeapSpaceInitial {
    public static void main(String[] args) {

        //返回Java虚拟机中的堆内存总量
        long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
        //返回Java虚拟机试图使用的最大堆内存量
        long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;

        System.out.println("-Xms : " + initialMemory + "M");
        System.out.println("-Xmx : " + maxMemory + "M");
    }
}

输出结果如下:

Java堆空间详解之新生代和老年代_第1张图片

因为我的系统内存是16G,所以对应的 243M 和3602M.

我们通过

设置堆空间的最小和最大内存之后,输出结果如下:

Java堆空间详解之新生代和老年代_第2张图片

看到这里大家也许会有疑问,为什么这里输出的数据会跟我们的实际分配的数据有一定的差别呢? 来,我们来详细探究一下.运行起来刚才的程序,然后我们通过命令行来进行查看

Java堆空间详解之新生代和老年代_第3张图片

这里 可以看到具体的堆空间的每个空间分配的大小情况.大家用电脑中的计算机进行加一下,可以发现.

S0C(S0区)+S1C(S1区)+EC(Eden区)+OC(老年代区) = 681984/1024 = 666M~ 

咦,这里怎么会又一样了呢, 那我们再做一下试验,S0C(S0区)+EC(Eden区)+OC(老年代区) = 653824/1024=638M,这个结果跟我们输出的结果是一样的.

这是因为在JVM规定当中,新生代区分为Eden区(伊甸园区)、S0区(Survivor0区,幸存者0区)S1区(Survivor1区,幸存者1区) ,而在两个幸存者区中只有一个空间会存放数据,另外一个空间是空的,所以在计算堆空间大小的时候,我们只计算一个空间大小.这也就是为什么会有一定偏差的原因

 通过上面的实验结果,我们也可以发现新生代区和老年代区的默认分配大小比例为1:2(222M:444M).我们可以通过参数-XX:NewRatio=a,来设置新生代和老年代的大小比例为1:a,

 

我们在通过随便跑一个没有设置过的分配比例的代码(如图,我随便用了一个EdenSurvivorTest方法),

代码如下:

public class EdenSurvivorTest {
    public static void main(String[] args) {
        System.out.println("进行测试,进行测试");
        try {
            //睡一会,方便我们查看命令
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Java堆空间详解之新生代和老年代_第4张图片

然后再cmd中打入命令jsp,查看当前运行的方法对应的pid,然后用命令 jinfo -flag NewRatio pid 来查看,得出结论,默认的新生区和老年区的大小比例为1:2.

对象分配过程:

首先,我们说一下,一个对象的分配一般过程,如图:

Java堆空间详解之新生代和老年代_第5张图片

对象最一开始创建的时候是分配在在Eden区中的,当我们不断的创建对象,Eden区不可避免的会存放满,这个时候就会触发YGC,而此时根据算法得出那些对象是还有引用的,这个时候就会将这些对象放入到Survivor区中的S0区(随机放入S0和S1,此处我们用S0举例子),此时S0区就是FROM区,S1区就是TO区. 这些放入到Survivor区中的对象会有一个age(年龄计数器),记录这个对象在Survivor区中存放的迭代次数.每次迭代之后,age+1.

随后在不断迭代这个过程中,如果Survivor区中有兑现给的age到达了 我们设置的-XX:MaxTenuringThreshold 次数(默认是15,但是不同的JVM和不同的GC ,此数值都不相同),此时我们就会将Survivor区中的对象放到到Old(老年代区).

通过代码来验证一下:

public class HeapInstanceTest {
    //创建一个随机大小的字节数组
    byte[] buffer = new byte[new Random().nextInt(1024 * 200)];

    public static void main(String[] args) {
        ArrayList list = new ArrayList();
        while (true) {
            list.add(new HeapInstanceTest());
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

程序最终会以OOM异常结束.在结束前,我们可以使用JDK自带的Java VisualVM工具来查看此时内存的情况:

Java堆空间详解之新生代和老年代_第6张图片

由图中可以看出.Eden区的大小是不断增多到达满值之后触发GC一下降低到0,然后继续增多....而对应的,Eden区GC之后,有引用的对象放入到了S0区,然后迭代次数多了之后Old区的对象开始多了起来.

我们再说一下对象分配的特殊情况,如图:

Java堆空间详解之新生代和老年代_第7张图片

当我们创建的新对象太大,超出了Eden区大小或者是本来Eden区就已经存放了一些数据的时候,此时新对象创建,判断Eden区放不下,这时候就会触发YGC,如果此时Eden区放得下,那么我们就会将之放入Eden区,这也是我们上面说的一般情况中的.

而如果是因为创建的新对象太大,YGC之后,Eden区依然放不下的话,我们就判断Old区是否能放下,如果能放下的话,直接分配到Old区,而如果放不下的话,我们就会触发FGC,FGC之后如果能放下,我们就会放入到Old区;FGC之后Old区依然放不下的话,JVM就会直接报出OOM异常,退出程序.

当我们对象放入Eden区之后,Eden区满,触发YGC,此时会将还有引用的对象放入到Survivor区,如果此时Survivor区放不下的话,就会直接晋升到老年代.

还有一种情况是,如果在Survivor区中相同年龄的对象的所有大小之和超过Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到-XX:MaxTenuringThreshold中要求的年龄。

你可能感兴趣的:(jvm,java,jdk,栈)