对于对象的回收,前面以及讲过具体的回收机制,下面我们来看看对象的分配策略!
①对象优先在Eden区域分配
大多数情况下,对象在新生代Eden区分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
虚拟机提供了-XX:PrintGCDetails这个收集日志参数,告诉虚拟机发生垃圾回收行为,并且在进程退出的时候适当输出当前内存各区域分配情况。
下面看下面的代码其中在运行前配置了参数 -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:PrintGCDetails -XX:SurvivorRatio=8,其中Xms20M -Xmx20M -Xmn10M 限制Java堆大小为20M,不可扩展,其中10M分配给新生代剩下10M分配给老年代,-XX:SurvivorRatio=8表示新生代中Eden区占8份(8M),Survivor区域占1份,剩余1份作为复制。下面我们来看看代码。
public class MinorGC {
private static final int _1MB = 1024 * 1024;
/**
* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
*/
public static void testAllocation() {
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
}
public static void main(String[] args) {
MinorGC gc=new MinorGC();
gc.testAllocation();
}
}
以下是产生的GC日志
[GC [DefNew: 6471K->137K(9216K), 0.0039742 secs] 6471K->6281K(19456K), 0.0040027 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4396K [0x266d0000, 0x270d0000, 0x270d0000)
eden space 8192K, 52% used [0x266d0000, 0x26af8fd8, 0x26ed0000)
from space 1024K, 13% used [0x26fd0000, 0x26ff2418, 0x270d0000)
to space 1024K, 0% used [0x26ed0000, 0x26ed0000, 0x26fd0000)
tenured generation total 10240K, used 6144K [0x270d0000, 0x27ad0000, 0x27ad0000)
the space 10240K, 60% used [0x270d0000, 0x276d0030, 0x276d0200, 0x27ad0000)
compacting perm gen total 12288K, used 361K [0x27ad0000, 0x286d0000, 0x2bad0000)
the space 12288K, 2% used [0x27ad0000, 0x27b2a7b8, 0x27b2a800, 0x286d0000)
ro space 8192K, 63% used [0x2bad0000, 0x2bfe8b20, 0x2bfe8c00, 0x2c2d0000)
rw space 12288K, 53% used [0x2c2d0000, 0x2c945138, 0x2c945200, 0x2ced0000)
Minor GC 主要发生在新生代的垃圾收集动作,因为java的大多数对象都是朝生熄灭的,Minor GC非常频繁,速度快
Full GC 主要发生在老年代,经常伴随Minor GC的发生。速度慢上倍。
②大对象直接进入老年代
所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组(下面例子种的byte{}[]就是典型的大对象)。大对象堆虚拟机的分配是一个坏消息,比遇到大对象更坏的消息就是遇到一群“朝生熄灭”的短命大对象,写程序时应当避免。经常出现大对象容易导致内存还有不少空间时需要提前触发垃圾收集以获取足够的连续空间来安装大对象。
虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的就是避免在 Eden区及两个Survivor区之间发生大量的内存复制。-XX:PretenureSizeThreshold=3145728 设置大于3M的对象之间放入老年代区。下面看代码
public class TestMaxTurningThreshold {
private static final int _1MB = 1024 * 1024;
/**
* VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails
-XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1 -XX:+PrintTenuringDistribution
-XX:PretenureSizeThreshold=3145728
*/
@SuppressWarnings("unused")
public static void testTenuringThreshold() {
byte[] allocation;
allocation = new byte[4 * _1MB];
}
public static void main(String[] args) {
TestMaxTurningThreshold a=new TestMaxTurningThreshold();
a.testTenuringThreshold();
}
}
结果4M的大对象因为超过3M直接进入老年代区。
三:长期存活的对象将进入老年代
既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代,为了做到这一点,虚拟机为每个对象定义一个对象年龄(Age)计数器。如果对象在Eden区每经过一次Minor GC后仍然存活,并且能被Survivor容纳的话,则移动到Survivor中,并且年龄+1,每经过一次Minor GC,对象年龄加一,大于设定的MaxTenuringThreshold的值则进入老年代,默认15;
-XX:MaxTenuringThreshold=10 设置年龄大于10的进入老年代区
四:动态年龄的判断
虚拟机并不是永远的要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,如果Survivor空间中的相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于改年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
五:空间分配担保
当新生代要进行Minor GC时候,首先会检查老年代最大的连续空间时候可以装载下新生代中的存活的对象,如果大于,则可以进行安全的Minor GC,否则,查看时候满足HandlePromationFailure 允许担保失败,检查老年代的最大连续空间是否大于平均晋升到老年代中的对象,如果大于,可以进行Minor GC,否则 需要老年代需要进行一个FullGC操作,使用标记-清除算法,为老年代提供更大的连续空间。