前言
Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。
回收对象内存是垃圾收集器的工作,在上一篇文章中已有阐述,这篇文章主要说一下对象的内存分配以及回收策略。
本文大纲:
- 1、对象优先分配在Eden区
- 2、大对象直接进入老年代
- 3、长期存活的对象将进入老年代
- 4、动态对象年龄判定
- 5、空间分配担保
- 6、总结
一、对象优先分配在Eden区
大多数情况下,对象都是优先在Eden区分配的,当Eden区没有足够的空间进行分配时,则虚拟机会进行GC回收(Minor GC)。
设置***堆和初始化堆都为20M,新生代分配10M,打印GC轨迹。
运行结果如下:
可以发现对象都被分配在Eden区,默认的Eden区与Survivor区比例是8,所以Eden区占8/10=8M,Survivor区有两个,每个都是1M。Eden区使用了56%,即5.6M,程序中对象obj和obj2各占2M,那多出来的1.6M是哪里来的?
原因是程序中的对象被存储时会被转换为虚拟机对象,而虚拟机对象包括对象头、对象的实例数据以及对齐填充。对象的实例数据可以理解为我们程序中分配的2M,多出来的1.6M自然就是对象头和对齐填充搞的事。
二、大对象直接进入老年代
大对象会被分配进老年代,可以通过虚拟机参数PretenureSizeThreshold来指定多大才算大对象。
设置***堆和初始化堆都为20M,新生代分配10M,打印GC轨迹,3M视为大对象。
运行结果如下,可以发现6M的对象被分配到了老年代(tenured generation)中。
三、长期存活的对象将进入老年代
长期存活的对象也会被晋升到老年代中,默认是15次的Minor GC年龄。意思就是一个对象在新生代中发生了15次的GC之后,如果还存活就会晋升为老年代对象。
这个年龄可以通过虚拟机参数MaxTenuringThreshold进行配置。
设置MaxTenuringThreshold=0即意味着只要新生代发现GC马上晋升为老年代对象。
运行结果如下,发现在***次GC的时候,对象obj和obj2都进入了老年代。
设置MaxTenuringThreshold=3即意味着要经过三次GC才可以晋升为老年代对象。
运行结果如下,发现这次只有obj2进入了老年代,对象obj2是因为太大在Survivor区存不下才进入老年代的。毫无悬念,对象obj留在了Survivor区。Eden存的是对象obj3。
四、动态对象年龄判定
为了更好地适应不同程序的内存状况,虚拟机并不是永远要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象的大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
首先为对象obj、obj2各分配256K内存,他们之和大于512K(因为虚拟机对象还包含对象头,所以是大于,不是等于),即大于Survivor的一半,所以会晋升为老年代。
运行结果如下,可以发现对象obj、obj2、obj3都进入老年代。对象obj3是因为太大Survivor存不下而进入老年代的。
为了更好的体验动态年龄的效果,作一个对比,这次设置为对象obj、obj2各分配128K内存,他们之和小于512K,即小于Survivor的一半,所以不会晋升为老年代。
运行结果如下,可以发现对象obj、obj2被存储于survivor区了。老年代存储的是对象obj3,Eden区存储的是***压入内存的obj4对象。
五、空间分配担保
在发生MinorGC之前,虚拟机会先检查老年代***可用的连续空间是否大于新生代所有对象的总空间,如果这个条件成立,即大于,那么MinorGC可以确保是安全的。当不大于时会有空间分配担保一说法。
空间分配担保是指上面的条件不成立时,如果允许空间分配担保,则虚拟机会进行一次MinorGC,而不是Full GC,尽管有可能内存溢出。如果不允许空间分配担保,则会进行一次FullGC,那停顿的时间就相对长很多了。一般FullGC的停顿时间是Minor GC的十倍。补充一点,是否允许空间分配担保可以通过虚拟机参数HandlePromotionFailure配置。
简而言之,投资总有风险,只不过空间分配担保的回报率很高,可以减少停顿时间,提高应用程序的效应速度。