3分钟带你秒懂对象的内存分配流程
01、背景介绍
在之前的文章中,我们介绍了类加载的过程、JVM 内存布局和对象的创建过程相关的知识。
本篇综合之前的知识,重点介绍一下对象的内存分配流程。
02、对象的内存分配原则
在之前的 JVM 内存结构布局的文章中,我们介绍到了 Java 堆的内存布局,由 年轻代 (Young Generation) 和老年代 (Old Generation) 组成,默认情况下按照1 : 2
的比例来分配空间。
其中年轻代又被划分为三个不同的区域:Eden 区、From Survivor 区、To Survivor 区,默认情况下按照8 : 1 : 1
的比例来分配空间。
Java 堆的内存布局,可以用如下图来概括。
当创建的对象不再被使用了是需要被回收掉的,以便腾出空间给新的对象使用,这就是对象的垃圾回收,也就是对象的 GC,我们会在后续的文章中再次介绍对象的垃圾回收算法以及垃圾收集器。
本次我们重点介绍下,创建不同大小的对象,在堆空间中发生的内存分配变化,以便后续更好的理解 GC 调优过程。
2.1、对象优先分配在 Eden 区
默认情况下,创建的对象会优先分配在年轻代的 Eden 区,当 Eden 区不够用的时候,会触发一次 Minor GC。
什么是 Minor GC 呢?
Minor GC 指的是 JVM 发生在年轻代的垃圾回收动作,效率高、速度快,但是只清除年轻代的垃圾对象。
与之对应的还有 Major GC 和 Full GC,Major GC 指的是 JVM 发生在老年代的垃圾回收动作,Major GC 的速度一般要比 Minor GC 慢 10 倍以上;同时,许多 Major GC 是由 Minor GC 引起的,因此把这个过程也称之为 Full GC,也就是对整个堆进行垃圾回收。
当 Eden 区满了以后,会发生 Minor GC,存活下来的对象会被移动到 Survivor 区,如果 Survivor 区装不下这批对象,此时会直接移动到老年代。
通常年轻代的对象存活时间都很短,在 Minor GC 后,大部分的对象都会被回收掉,但是也不排除个例情况,存活下来的对象的年龄会进行 +1,当年龄达到 15岁时,也会被移动到老年代。
用户可以通过-XX:MaxTenuringThreshold
参数来调整年龄的阀值,默认是 15,最大值也是 15。
2.2、大对象直接进入老年代
所谓大对象,顾名思义就是占用内存比较多的对象,大对象一般可以直接分配到老年代,这是 JVM 的一种优化设计。
用户可以手动通过-XX:PretenureSizeThreshold
参数设置大对象的大小,默认是 0,意味着任何对象都会优先在年轻代的 Eden 区分配内存。
试想一下,假如大对象优先在 Eden 区中分配,给其它的对象预留的空间就会变小,此时很容易触发 Minor GC,经过多次 GC 之后,大对象可能会继续存活,最终还是会被转移到老年代。
与其如此,还不如直接分配到老年代。
2.3、对象动态年龄判断机制
对象动态年龄判断,简单的说就是对 Survivor 区的对象年龄从小到大进行累加,当累加到 X 年龄(某个年龄)时占用空间的总和大于 50%,那么比 X 年龄大的对象都会移动到老年代。
这种机制是 JVM 的一个预测机制,虚拟机并不是完全要求对象年龄必须达到 15 才能移动到老年代。当 survivor 区快要满了并且存在一批可能会长期存活的对象,那不如提前进入老年代,减少年轻代的压力。
用户可以使用-XX:TargetSurvivorRatio
参数来设置保留多少空闲空间,默认值是 50。
2.4、逃逸分析
逃逸分析是一项比较前沿的优化技术,它并不是直接优化代码的手段,而是为其它优化手段提供了分析技术。
什么是逃逸分析呢?
当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸。
如果能证明一个对象不会逃移到方法外或者线程之外,换句话说就是别的方法或线程无法通过任何途径访问到这个对象,虚拟机可以通过一些途径为这个变量进行一些不同程度的优化。
比如栈上分配、同步消除、标量替换等优化操作。
2.4.1、栈上分配
在上文我们提及到,对象会优先在堆上分配,垃圾收集器会定期回收堆内存中不再使用的对象,但这块的内存回收很耗时。
如果确定一个对象不会逃逸出方法之外,让这个对象在栈上分配,对象所占用的内存空间就可以随着栈帧出栈而销毁,这样垃圾收集器的压力将会小很多。
2.4.2、同步消除
线程同步本身是一个相对耗时的操作,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争,此时虚拟机会对这个变量,实施的同步措施进行消除,比如去掉同步锁来运行方法。
2.4.3、标量替换
标量是指一个数据已经无法再分解成更小的数据来表示了,比如 Java 虚拟机中的原始数据类型(int,long 等数值类型以及 reference 类型)等都不能进一步分解,它们可以称为标量。相对的,如果一个数据可以继续分解,那它称为聚合量。
Java 中最典型的聚合量是对象,如果逃逸分析证明一个对象不会被外部访问,并且这个对象是可分解的,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替,拆散后的变量便可以被单独分析与优化,可以各自分别在栈帧或寄存器上分配空间,原本的对象就无需整体分配空间了。
默认情况下逃逸分析是关闭的,用户可以使用-XX:+DoEscapeAnalysis
参数来手动开启逃逸分析。
03、小结
综合以上的内容,对象的内存分配流程,可以用如下图来概括。
由此可知,对象在内存分配的时候,会根据不同情况来判断合理分配,以便 JVM 更快捷的进行回收资源。
04、参考
1.https://zhuanlan.zhihu.com/p/267223891
2.https://zhuanlan.zhihu.com/p/401057707
3.https://www.cnblogs.com/xrq730/p/4827590.html
4.https://www.jianshu.com/p/3d38cba67f8b
6.https://blog.csdn.net/clover_lily/article/details/80095580