对象基本上是在堆上分配(除了JIT即时编译器优化),分配规则如下:
-
主要分配在新生代的Eden区上;
-
若开启了本地线程分配缓冲TLAB,按线程优先在TLAB上分配;
-
少数情况直接分配在老年代
1、对象优先在Eden上分配
目前主流的GC都采用了分代回收算法,因此将内存分为了新生代和老年代。新生代一般采取“复制”算法,考虑效率和避免内存碎片的问题。
堆内存的新生代被进一步分为:Eden+From Survivor+To Survivor。
-
对象首先分配在Eden区;
-
若Eden区放不下这个对象时,会触发一次Minor GC,回收Eden + From Survivor中无效的对象,并将存活的对象放置于To Survivor中;
-
若To Survivor空间放不下存活对象,就会启用“分配担保”,将这些存活对象转移至老年代中,然后再将新对象存入Eden区。
2、大对象直接进入老年代
大对象是指,需要大量连续内存空间的Java对象,比如:很长的字符串及数组。
我们知道:一个大对象能存入Eden区+Survivor1区的概率较小,发生分配担保概率较大,而分配担保涉及大量的复制操作,导致效率低下。
解决:将大对象直接放到老年代,从而避免大量复制操作。
通过-XX:PretrnureSizeThreshold参数设置大对象
该参数用于设置大小超过该参数的对象被认为是“大对象”,直接进入老年代。
注意:该参数只对Serial和ParNew收集器有效。
3、长期存活的对象将进入老年代
老年代用于存储生命周期较长的对象,那么如何判断一个对象的年龄?
新生代的每个对象都有一个对象年龄计数器,
-
对象在Eden出生,每经过一次Minor GC还存活,对象年龄就加一;
-
当年龄超过一定值时,就将超过该值的新生代对象转移到老年代。
使用-XX:MaxTenuringThreshold参数设置新生代的最大年龄
只要超过该值,对象就会转移到老年代中。
动态对象年龄判断
目的:为了更好适应不同程序的内存状况
如果在当前新生代的Survivor中,年龄相同的对象的内存空间总和超过了Survivor内存空间的一半,那么年龄大于或者等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
4、空间分配担保
发生Minor GC之前,虚拟机首先会检查“老年代中最大连续区域的大小 是否大于 新生代中所有对象的总大小”,
也就是说老年代目前能否一定能将新生代中的所有对象全部装下?
-
若能装下,此时进行Minor GC没有任何风险,然后进行Minor GC;
-
若不一定能装下,此时进行Minor GC是有风险的,检查HandlePromotionFailure设置值是否允许担保失败,
-
若允许,那么检查老年代最大可用的连续空间 是否大于 历次晋升到老年代对象的平均大小;
-
若大于,尝试进行Minor GC,尽管这次Minor GC是有风险的;
-
若小于,改为进行一次Full GC。
-
-
不允许,改为进行一次Full GC,清除老年代的废弃数据来扩大老年代的空闲空间,以便给新生代作分配担保。
-
“风险”是指什么?
-
新生代使用“复制”算法,需要分配担保;而老年代使用的是“标记-整理”算法,不需要。
-
在Minor GC后,当Survivor内存不够时,老年代要进行担保,必须要有足够空间存放存活对象;
-
但是有多少对象活下来在回收之前时不知道的,所以取 “历次晋升到老年代对象的平均大小”作为经验值,决定是否用Full GC来让老年代腾出更多空间;
-
若最终还是担保失败了,那么失败之后重新发起一次Full GC。为了防止Full GC过于频繁,HandlePromotionFailure一般设置为打开。