内存分配策略
Java技术体系的自动内存管理最终可以归结为自动化地解决两个问题:给对象分配内存以及回收分配给对象的内存。对象的内存分配,往大方向上讲,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中,分配的规则并不是百分百固定的,其细节取决于当前使用的哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数设置。
下面将会讲几条最普遍的内存分配规则,并通过代码去验证这些规则。代码在测试时使用Client模式虚拟机运行,验证的是使用Serial/Serial Old收集器下的内存分配和回收策略。
-
对象优先在Eden分配
大多数情况下,对象在新生代Eden中分配,当Eden没有足够空间进行分配时,虚拟机将发起一次Minor GC(新生代GC:指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快)。虚拟机提供了-XX:PrintGCDetails这个收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前内存各区域的分配情况。public class HeapGC { private static final int _1MB = 1024 * 1024; /** * @param args */ public static void main(String[] args) { 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]; } }
运行时设置VM的参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC。代码的main方法中,尝试分配3个2MB大小和1个4MB大小的对象,在运行时通过-Xms20M、 -Xmx20M和 -Xmn10M这3个参数限制Java堆大小为20MB,且不可扩展,其中10MB分配给新生代,剩下的10MB分配给老年代。-XX:SurvivorRatio=8决定了新生代中Eden区与一个Survivor区的空间比例是8:1,-XX:+UseSerialGC设置虚拟机使用Serial/Serial Old收集器。执行结果为:[GC [DefNew: 6471K->139K(9216K), 0.0094000 secs] 6471K->6283K(19456K), 0.0094840 secs] [Times: user=0.01 sys=0.01, real=0.01 secs] Heap def new generation total 9216K, used 4563K [0xaa0c0000, 0xaaac0000, 0xaaac0000) eden space 8192K, 54% used [0xaa0c0000, 0xaa511fa8, 0xaa8c0000) from space 1024K, 13% used [0xaa9c0000, 0xaa9e2d00, 0xaaac0000) to space 1024K, 0% used [0xaa8c0000, 0xaa8c0000, 0xaa9c0000) tenured generation total 10240K, used 6144K [0xaaac0000, 0xab4c0000, 0xab4c0000) the space 10240K, 60% used [0xaaac0000, 0xab0c0030, 0xab0c0200, 0xab4c0000) compacting perm gen total 16384K, used 2394K [0xab4c0000, 0xac4c0000, 0xb34c0000) the space 16384K, 14% used [0xab4c0000, 0xab716888, 0xab716a00, 0xac4c0000) No shared spaces configured.
执行到分配allocation4对象的语句时会发生一次Minor GC,这次GC的结果是新生代6471K变为139K,而总内存占用量几乎没有减少,因为allocation1、2、3三个对象都是存活的,虚拟机几乎没有找到可回收的对象。这次GC发生的原因是给allocation4分配内存的时候,发现Eden已经被占用了6MB,剩余空间已不足以给allocation4分配所需的4MB内存,因此发生Minor GC。GC期间虚拟机又发现已有的3个2MB大小的对象全部无法放入到Survivor空间(Survivor空间只有1MB大小),所以只好通过分配担保机制提前转移到老年代去。
这次GC结束后,4MB的allocation4对象被顺利分配在Eden中。因此程序执行完的结果是Eden占用4MB,Survivor空闲,老年代被占用6MB。通过日志可以证实这一点。 -
大对象直接进去老年代
所谓大对象就是指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串及数组。经常出现大对象容易导致内存在还有不少空间时就提前触发垃圾收集以获得足够的连续空间来安置他们。
虚拟机提供了一个-XX:PretenureSizeThreshold参数,使大于这个设置值的对象直接在老年代中分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存拷贝。设置了-XX:PretenureSizeThreshold=3145728后再执行上面的代码。Heap def new generation total 9216K, used 6635K [0xaa0d0000, 0xaaad0000, 0xaaad0000) eden space 8192K, 81% used [0xaa0d0000, 0xaa74aef0, 0xaa8d0000) from space 1024K, 0% used [0xaa8d0000, 0xaa8d0000, 0xaa9d0000) to space 1024K, 0% used [0xaa9d0000, 0xaa9d0000, 0xaaad0000) tenured generation total 10240K, used 4096K [0xaaad0000, 0xab4d0000, 0xab4d0000) the space 10240K, 40% used [0xaaad0000, 0xaaed0010, 0xaaed0200, 0xab4d0000) compacting perm gen total 16384K, used 2387K [0xab4d0000, 0xac4d0000, 0xb34d0000) the space 16384K, 14% used [0xab4d0000, 0xab724ca0, 0xab724e00, 0xac4d0000) No shared spaces configured.
我们可以看到这次没有发生Minor GC操作,Eden占用6MB,而老年代的空间被占用了4MB,也就是allocation4被直接分配在老年代中,这是因为PretenureSizeThreshold被设置为3MB(这个参数不能与-Xmx之类的参数一样直接写3MB),因此超过3MB的对象会直接在老年代中进行分配。 -
长期存活的对象将进入老年代
虚拟机既然采用了分代收集的思想来管理内存,那内存回收时就必须能识别哪些对象应该放在新生代,哪些应该发在老年代中。为了做到这点,虚拟机给每个对象定义了一个对象年龄计数器,如果对象在Eden出生并经过一次MInor GC后仍然存活,并且能被Survivor容纳的话,将会被移动至Survivor空间中,并将对象的年龄增加到一定程度(默认为15岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阀值,可以通过参数-XX:MaxTenuringThreshold来设置。public class MinorGC { private static final int _1MB = 1024 * 1024; /** * @param args */ public static void main(String[] args) { byte[] allocation1, allocation2, allocation3; allocation1 = new byte[_1MB / 4]; // 什么时候进入老年代取决于XX:MaxTenuringThreshold设置 allocation2 = new byte[4 * _1MB]; allocation3 = new byte[4 * _1MB]; allocation3 = null; allocation3 = new byte[4 * _1MB]; } }
设置 -XX:+PrintTenuringDistribution,打印GC后新生代各个对象的年龄,设置-XX:MaxTenuringThreshold=1后执行代码[GC [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1) - age 1: 404752 bytes, 404752 total : 4679K->395K(9216K), 0.0061420 secs] 4679K->4491K(19456K), 0.0061850 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] [GC [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1) - age 1: 136 bytes, 136 total : 4655K->0K(9216K), 0.0008720 secs] 8751K->4491K(19456K), 0.0009020 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4260K [0xaa110000, 0xaab10000, 0xaab10000) eden space 8192K, 52% used [0xaa110000, 0xaa538fe0, 0xaa910000) from space 1024K, 0% used [0xaa910000, 0xaa910088, 0xaaa10000) to space 1024K, 0% used [0xaaa10000, 0xaaa10000, 0xaab10000) tenured generation total 10240K, used 4490K [0xaab10000, 0xab510000, 0xab510000) the space 10240K, 43% used [0xaab10000, 0xaaf72bc0, 0xaaf72c00, 0xab510000) compacting perm gen total 16384K, used 2394K [0xab510000, 0xac510000, 0xb3510000) the space 16384K, 14% used [0xab510000, 0xab766860, 0xab766a00, 0xac510000) No shared spaces configured.
设置-XX:MaxTenuringThreshold=15后执行代码[GC [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15) - age 1: 404752 bytes, 404752 total : 4679K->395K(9216K), 0.0078860 secs] 4679K->4491K(19456K), 0.0079300 secs] [Times: user=0.01 sys=0.01, real=0.01 secs] [GC [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15) - age 1: 136 bytes, 136 total - age 2: 404400 bytes, 404536 total : 4655K->395K(9216K), 0.0008870 secs] 8751K->4491K(19456K), 0.0009180 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4655K [0xaa040000, 0xaaa40000, 0xaaa40000) eden space 8192K, 52% used [0xaa040000, 0xaa468fe0, 0xaa840000) from space 1024K, 38% used [0xaa840000, 0xaa8a2c38, 0xaa940000) to space 1024K, 0% used [0xaa940000, 0xaa940000, 0xaaa40000) tenured generation total 10240K, used 4096K [0xaaa40000, 0xab440000, 0xab440000) the space 10240K, 40% used [0xaaa40000, 0xaae40010, 0xaae40200, 0xab440000) compacting perm gen total 16384K, used 2394K [0xab440000, 0xac440000, 0xb3440000) the space 16384K, 14% used [0xab440000, 0xab696860, 0xab696a00, 0xac440000) No shared spaces configured.
方法中的allocation1需要256KB的内存空间,Survivor空间可以容纳。当MaxTenuringThreshold=1时,allocation1在第二次GC发生时进入老年代,新生代已使用的内存GC后会变为0KB。而MaxTenuringThreshold=15时,第二次GC发生后,allocation1对象则还是留在新生代Survivor空间,这时候新生代仍然有395KB的空间被占用。 -
动态对象年龄判定
为了能更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,那么年龄大于或等于该年龄的对象就可以直接进入老年代,无须等待MaxTenuringThreshold中要求的年龄。public class MinorGC { private static final int _1MB = 1024 * 1024; /** * @param args */ public static void main(String[] args) { byte[] allocation1, allocation2, allocation3, allocation4; allocation1 = new byte[_1MB / 4]; // allocation1+allocation2大于Survivor空间的一半 allocation2 = new byte[_1MB / 4]; allocation3 = new byte[4 * _1MB]; allocation4 = new byte[4 * _1MB]; allocation4 = null; allocation4 = new byte[4 * _1MB]; } }
设置参数MaxTenuringThreshold=15运行代码[GC [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 15) - age 1: 666912 bytes, 666912 total : 4935K->651K(9216K), 0.0062200 secs] 4935K->4747K(19456K), 0.0062660 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] [GC [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15) - age 1: 136 bytes, 136 total : 4911K->0K(9216K), 0.0010640 secs] 9007K->4747K(19456K), 0.0010960 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4260K [0xaa040000, 0xaaa40000, 0xaaa40000) eden space 8192K, 52% used [0xaa040000, 0xaa468fe0, 0xaa840000) from space 1024K, 0% used [0xaa840000, 0xaa840088, 0xaa940000) to space 1024K, 0% used [0xaa940000, 0xaa940000, 0xaaa40000) tenured generation total 10240K, used 4746K [0xaaa40000, 0xab440000, 0xab440000) the space 10240K, 46% used [0xaaa40000, 0xaaee2bd0, 0xaaee2c00, 0xab440000) compacting perm gen total 16384K, used 2394K [0xab440000, 0xac440000, 0xb3440000) the space 16384K, 14% used [0xab440000, 0xab696890, 0xab696a00, 0xac440000) No shared spaces configured.
运行结果中Survivor的空间仍然为0%,而老年代比预期增加了6%,也就是说allocation1、allocation2对象直接进入了老年代,而没有等到15岁的临界年龄,因为这两个对象加起来已经达到了512KB,并且他们是同年的,满足同年对象达到Survivor空间一半的规则。我们只要注视掉其中一个对象的new操作,就会发现另外一个不会晋升到老年代中。 -
空间分配担保
在发生Minor GC时,虚拟机会检测之前晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC(指发生在老年代的GC,也可称为Major GC,它的执行速度一般会比Minor GC慢10倍以上。)。如果小于,则查看HandlePromotionFailure设置是否允许担保失败,如果允许,那么只会进行Minor GC;如果不允许,则也要改为进行一次Full GC。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。public class MinorGC { private static final int _1MB = 1024 * 1024; /** * @param args */ public static void main(String[] args) { byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7; allocation1 = new byte[2 * _1MB]; allocation2 = new byte[2 * _1MB]; allocation3 = new byte[2 * _1MB]; allocation1 = null; allocation4 = new byte[2 * _1MB]; allocation5 = new byte[2 * _1MB]; allocation6 = new byte[2 * _1MB]; allocation4 = null; allocation5 = null; allocation6 = null; allocation7 = new byte[2 * _1MB]; } }
这个HandlePromotionFailure参数设置跟虚拟机的版本有关,我设置后无效Warning: The flag HandlePromotionFailure=true has been EOL'd as of 6.0_24 and will be ignored,大家可以用低版本的虚拟机试试。不同的收集器,内存分配策略也略有差别,大家可以换其他的收集器执行上面的代码,看看差别。