垃圾收集器与内存分配策略-内存分配与回收策略
如何去给对象分配内存?大方向讲,就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地在栈上分配)
①对象优先在Eden分配
大多数条件下,对象在Eden中分配,当Eden内存不够的时候,虚拟机将发起一次Minor GC。
private static final int _1MB=1024*1024; /** * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M (限制了java堆大小为20MB,新生代10MB,老年代10MB)-XX:+PrintGCDetails(虚拟机发生GC时会收集日志) -XX:SurvivorRatio=8(规定Eden与Survivor比例为8/1) */ public static void test{ byte[] allocation1,allocation2,allocation3,allocation4; allocation1=new byte[2*_1MB]; allocation2=new byte[2*_1MB]; allocation3=new byte[2*_1MB]; allocation4=new byte[4*_1MB];//出现一次Minor GC }
java堆占10MB,Eden占8MB,创建前三个对象在Eden中占了6MB,当创建第四个对象时,第四个对象4MB,Eden区无法分配足够内存,所以发生一次GC,虚拟机又发现前三个对象无法放到Suvivor空间中,因为一个Suvivor空间只有1MB,所以通过分配担保机制直接到老年代去。
② 大对象直接进入老年代
大对象是指需要大量连续内存空间的Java对象(比如很长的字符串和数组)。经常出现大对象就会提前触发GC以获取连续的空间来放置他们。
/** * *VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M *-XX:+PrintGCDetails -XX:SurvivorRatio=8 *-XX:PretenureSizeThreshold=2145728(令大于这个值得对象直接在老年代中分配,只对Serial和ParNew两款收集器有效) */ public class aaa { private static final int _1MB=1024*1024; public static void main(String[] args){ byte[] allocation; allocation=new byte[4*_1MB]; } }
③长期存活的对象将进入老年代
为了实现分代手机的思想,内存回收时就应该能识别哪些对象应放在新生代,哪些对象应放在老年代。为了达到这个目的,虚拟机给每个对象定义了一个对象年龄计数器。
当对象在Eden出生并经过第一次存活并能移动到Surivor中,将年龄设为1,在Surivor中每经过一次GC,年龄加1,等到了默认年龄15岁(可以通过-XX:MaxTenuringThreshold方法改变这个值),就会被移动到老年代。
④ 动态对象年龄判定
为了适应不同程序得内存状况,虚拟机不是永远要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄的所有对象大小的总和大于Survior空间的一半,年龄大于或等于该对象就可以直接进入老年代
⑥ 空间分配担保
在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以保证是安全的。如果不成立,虚拟机会查看HandlePromotionFailure设置值是否允许担保失败,如果允许,那么就继续检查,看看老年代最大的连续可用空间是不是比历次新生代往老年代传的平均值大,如果大于,将会尝试一次Mionr GC,但是这是有风险的,如果小于,或者HandlePromotionFailure设置不允许冒险,就会改为进行一次Full GC。
如果不管HandlePromotionFailure的阻拦,非要进行Minor GC,到底会冒什么风险呢?
复制算法时提到,为了内存利用率,只是用其中一个Survior进行轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况,就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成回收之前是没有办法确定的所以只好取之前每一次回收晋升到老年代对象的平均值大小作为依据,与老年代剩余空间进行比较,决定是否Full GC来让老年代腾出更多空间。
取平均值进行比较仍然是一种动态概率手段,总会出现某一次突然比平均值大很多的情况,所以依然会导致担保失败,如果出现了担保失败,那么就只好重新发起一次Full GC,虽然担保失败时绕的圈子是最大的,但大部分情况下还是会将HandlePromotionFailure开关打开,避免Full GC太过于频繁。