1.优先分配到Eden区

Java虚拟机会优先把新new出来的对象放在新生代这块内存中,口说无凭,下面我们来验证一下。指定虚拟机参数-verbose:gc -XX:+PrintGCDetails把详细的垃圾回收信息打印出来。

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

    }

}

运行这个类,输出如下:

java 占用虚拟内存 java使用虚拟内存_java 占用虚拟内存

从输出我们可以看到没申请之前新生代占了8%的内存。

再此运行下面这个程序,我们创建一个20M的对象。

public class Main {
    public static void main(String[] args) {
       byte[] b1 = new byte[20 * 1024 * 1024];
    }

}

再次运行,输出如下:

java 占用虚拟内存 java使用虚拟内存_内存分配_02

我们可以看到这次新生代的内存被占用了39%,比上一次多占了31%,31%乘上66048k大约是20M,由此可见确实是在新生代分配了新建new对象的内存。

我们再探究一下优先的含义,在虚拟机参数中加入 -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8,分别指定最小堆20M,最大堆20M,新生代10M,其中Eden占8/10即8M,Survivor区占2/10即2M,from区和to区分别占1M。

public class Main {
    public static void main(String[] args) {
        byte[] b1 = new byte[2 * 1024 * 1024];
        byte[] b2 = new byte[2 * 1024 * 1024];
        byte[] b3 = new byte[2 * 1024 * 1024];
        byte[] b4 = new byte[4 * 1024 * 1024];
    }

}

如上面的程序,我们分别创建了三个2M的对象和一个4M的对象,运行该程序,输出如下:

java 占用虚拟内存 java使用虚拟内存_垃圾回收_03

可以看出,新生代8192K的内存占用了93%,大概是6M多,即前三个对象分配到了Eden区域,老年代10240k占用了40%,即4M,最后一个对象分配到了老年代。这是怎么回事呢?其实是这样的,Java虚拟机在分配了前三个对象的内存后,在分配最后一个对象的内存时,发现内存不够用了,于是触发了内存担保,将最后一个对象分配到了老年代。

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

保持上面的虚拟机参数,运行下面的程序:

public class Main {
    public static void main(String[] args) {
        byte[] b4 = new byte[6 * 1024 * 1024];
    }

}

java 占用虚拟内存 java使用虚拟内存_垃圾回收_04

从输出可以看到6M的对象还是分配在新生代的,再运行下面的程序:

public class Main {
    public static void main(String[] args) {
        byte[] b4 = new byte[7 * 1024 * 1024];
    }

}

java 占用虚拟内存 java使用虚拟内存_内存分配_05

这次7M的对象被分配到了老年代,7M的对象虚拟机认为是大对象,那么这个值是怎么确定的呢,我们是否可以更改呢?我们可以通过指定虚拟机参数-XX:PertureSizeThreshold来指定对象进入老年代的阈值,下面我们通过指定此参数为5M来看看上面刚刚被分配在新生代的6M对象会不会分到老年代。

java 占用虚拟内存 java使用虚拟内存_内存分配_06

老年代被用了60%,可以看出设置的阈值生效了,超过5M的对象虚拟机认为是大对象,优先分配在老年代。大对象优先分配到老年代是因为新生代发生GC的次数很频繁,并且新生代采用的是复制的垃圾收集算法,对于大对象来说,复制会很耗时间,GC的时间会变长。

3.长期存活的对象分配到老年代

我们可以通过虚拟机参数--XX:MaxTenuringThreshold来指定一个对象经历多少次垃圾回收后进入老年代。其默认值是15,也就是说当新创建的一个对象经历过15次垃圾回收后,就进入老年代。

4.空间分配担保

当创建一个对象时,虚拟机发现新生代内存不够用时,会向老年代申请空间分配担保。这时,虚拟机会检查此时的老年代最大可用连续空间够不够容纳新生代的所有内存,如果不够则检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,如果不大于则不会进行内存分配担保,如果大于还会检查参数是--XX:+HandlePromotionFailure还是--XX:-HandlePromotionFailure(+表示开启,-表示关闭),如果是开启则进行内存分配担保,如果是关闭依旧不会进行内存分配担保。虚拟机默认开启内存分配担保。

5.逃逸分析与栈上分配

前面的文章中说到过虚拟机的内存模型,其中有一块是虚拟机栈,它使用一种栈帧的数据结构管理着Java方法的调用,方法的调用伴随着栈帧的出栈与入栈,如果说对象的内存能分配到栈上的话,对象的内存会随着方法执行的结束而回收,如此以来就不再需要垃圾回收,因此能提高代码性能。但对象的栈上分配是有条件的,它要求对象不能逃逸,若一个对象只在方法体内有效则对象没有逃逸,其他情况都判定为对象逃逸。下面举几个对象逃逸分析的例子:

public class StackAllocation {
    public StackAllocation stackAllocation;

    /**
     *返回StackAllocation对象,发生逃逸
     */
    public StackAllocation getStackAllocation() {
        return stackAllocation == null ? new StackAllocation() : stackAllocation;
    }

    /**
     * 为成员赋值,发生逃逸
     */
    public void setStackAllocation() {
        this.stackAllocation = new StackAllocation();
    }

    /**
     * 对象的作用域仅在当前方法中有效,没有发生逃逸
     */
    public void useStackAllocation() {
        StackAllocation stackAllocation = new StackAllocation();
    }

    /**
     * 引用成员变量的值,发生逃逸
     */
    public void useStackAllocation2() {
        StackAllocation stackAllocation = getStackAllocation();
    }
}