对象的分配策略

对象的分配策略

  • 对象的分配原则
    • 对象优先在Eden区分配
    • 大对象直接进入老年代
    • 长期存活对象进入老年区
    • 对象年龄动态判断
  • 空间分配担保
  • 逃逸分析
    • 逃逸分析的原理

对象的分配原则

对象的分配策略_第1张图片

对象优先在Eden区分配

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

大对象直接进入老年代

大对象就是指需要大量的连续内存空间的Java对象,最典型的大对象便是那种很长很长的字符串,或者元素数量庞大的数组。
大对象对虚拟机的内存分配来说就是一个不折不扣的坏消息,比遇到一个大对象更坏的消息是一群朝生夕灭短命大对象,写程序一定要避免。
为什么要避免大对象,因为在分配空间时,它容易导致内存明明还有不少空间时就提前触发垃圾收集,以获取足够的连续空间才能安置好他们,而当复制对象时,大对象就意味着高额的内存复制开销。
HotSpot 提供了-XX:PretenureSizeThreshold 参数,指定大于该设置值的对象直接在老年代分配 ,这样做的目的:
1、避免大量内存复制(避免Eden区及两个survivor区之间来回复制,产生大量的内存复制操作)
2、避免提前进行垃圾回收。

长期存活对象进入老年区

HotSpot 虚拟机中多数收集器都采用了分代收集来管理堆内存,那内存回收时就必须能决策哪些存活的对象应当放在新生代,哪些存活的对象存放下老年代。
为了坐到这点,虚拟机给每个对象定义了一个对象年龄计数器(Age),存储在对象头中。
对象的分配策略_第2张图片
对象的分配策略_第3张图片
如果对象在Eden 出生并经过第一次MInor GC 后仍然存活,并且被Survivor 区容纳的话,将被移动到 Survivor 空间中,并将年龄加一,对象在 Survivor区每经过一次 Minor GC,年龄就增加 1,当它的年龄增加到一定程度(并发的垃圾回收器默认为 15),CMS 是 6 时,就会被晋升到老年代中。
-XX:MaxTenuringThreshold 参数可以调整

对象年龄动态判断

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

空间分配担保

在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么 Minor GC 可以确保是安全 的。如果不成立,则虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历 次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,尽管这次 Minor GC 是有风险的,如果担保失败则会进行一次 Full GC;如果小 于,或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC。

逃逸分析

逃逸分析的原理

分析对象动态作用域,当一个对象在方法中定义后,它可能被外部方法所引用。
比如:调用参数传递到其他方法中,这种称之为方法逃逸。甚至还有可能被外部线程访问到,例如:赋值给其他线程中访问的变量,这个称之为线程逃逸。 从不逃逸到方法逃逸到线程逃逸,称之为对象由低到高的不同逃逸程度。 如果确定一个对象不会逃逸出线程之外,那么让对象在栈上分配内存可以提高 JVM 的效率。

上代码

public class Test {

    public static void main(String[] args) throws Exception {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 500000000; i++) {
            allocate();
        }
        System.out.println(System.currentTimeMillis() - start +"毫秒");
        Thread.sleep(600000);
    }

    static void allocate() {
        MyOject myOject = new MyOject(2021, 2021.1314);
    }

    static class MyOject {
        int a;
        double b;

        MyOject(int a, double b) {
            this.a = a;
            this.b = b;
        }

    }

}

-XX:-DoEscapeAnalysis (关闭逃逸分析)
执行结果:
对象的分配策略_第4张图片
-XX:+DoEscapeAnalysis (开启逃逸分析,默认开启)
执行结果:
对象的分配策略_第5张图片
测试结果可见,开启逃逸分析对代码的执行性能有很大的影响!那为什么有这个影响?

如果是逃逸分析出来的对象可以在栈上分配的话,那么该对象的生命周期就跟随线程了,就不需要垃圾回收,如果是频繁的调用此方法则可以得到很大的性能提高。 采用了逃逸分析后,满足逃逸的对象在栈上分配

对象的分配策略_第6张图片
没有开启逃逸分析,对象都在堆上分配,会频繁触发垃圾回收(垃圾回收会影响系统性能),导致代码运行慢
对象的分配策略_第7张图片
上代码(上面同样代码)
开启逃逸分析-XX:+DoEscapeAnalysis ,开启 GC 打印日志 -XX:+PrintGC

运行结果:

对象的分配策略_第8张图片
没有GC 日志,说明没有触发垃圾回收

关闭逃逸分析-XX:-DoEscapeAnalysis ,开启 GC 打印日志 -XX:+PrintGC

对象的分配策略_第9张图片
可以看到关闭了逃逸分析,JVM 在频繁的进行垃圾回收(GC),正是这一块的操作导致性能有较大的差别。

你可能感兴趣的:(JVM笔记,jvm)