前面的文章介绍了对象的创建过程,其中第三步 —— 分配内存,只是简单的介绍了分配的方式 —— 指针碰撞、空闲列表,其实内存在堆上分配还大有文章嘞。
对象的内存分配,往大方向上讲,就是在堆上分配,对象主要分配在新生代的 Eden 区上,如果启动了本地线程分配缓冲,将按线程优先在 TLAB 上分配。少数情况下也可能直接分配在老年代中,分配的规则并不是百分之百固定的。其细节取决于当前使用的是哪一种垃圾收集器的组合,还有虚拟机中与内存相关的参数的设置。
下面介绍一些常见的内存分配策略。
虚拟机将新生代内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间(默认比例是 8:1:1),大多数情况下,分配对象时,使用 Eden 和其中一块 Survivor 空间,当没有足够空间进行分配时,虚拟机将会进行一次 MinorGC。
如果虚拟机打开了 TLAB,那么对象优先在 TLAB 上分配。TLAB 全称是本地线程分配缓冲(Thread Local Allocation Buffer),它是每个线程在 Java 堆中预先分配的一小块内存。因为 TLAB是线程私有的,没有锁开销,因此性能较好,在 JDK7 之后默认开启。
虚拟机提供了一个 -XX:PretenureSizeThreshold 参数,令大于这个设置值的对象直接在老年代分配,这样做的目的是避免在 Eden 区和及两个 Survivor 区之间发生大量的内存复制。注意!该参数只对 Serial 和 ParNew 收集器有效,Parallel Scavenge 并不认识该参数。
一般我们代码中常见的大对象是指那种很长的字符串以及数组,写程序的时候应当避免,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的内存空间来“安置”它们。
还记得前面文章我们介绍对象创建的时候,说到对象头中有一个 “GC 分代年龄” 吗?那么这个是做啥用的呢?
如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并且对象年龄设为1。对象在 Survivor 空间中每“熬过”一次 Minor GC,年龄就增加 1 岁,当它的年龄到达一定程度(最大为 15 岁),就将会被晋升到老年代。对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 设置。
对象是否能够晋升到老年代,也不全由 -XX:MaxTenuringThreshold 参数控制,如果 Survivor 空间中相同年龄的所有对象大小总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
新生代在发生 Minor GC 之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象之和(或者历次晋升老年代对象的平均大小)。如果这个条件不成立,那么虚拟机将直接进行 Full GC 动作;如果这个条件成立,那么虚拟机就会进行一次 Minor GC 操作,但是这次 Minor GC 是有风险的,因为比较的值是平均值,可能出现极端的情况 —— 大量对象在 Minor GC 后还存活,这时就只好在失败后重新发起一次 Full GC。