为什么需要?
新生代采用的复制算法,留空一个 survivor 作为空间备份,当大量对象在 minor gc后仍然存活,survivor 无法放下,则会直接进入老年代, 需要老年代的空间保证能容纳得下这些对象。
如何担保?
空间担保比较的是 老年代最大的连续空闲空间 和 年轻代所有对象的内存大小 或 历次晋升到老年代的平均对象大小做比较
为什么需要是连续的空间?
分配担保机制中,无论是新生代所有对象总和还是历次晋升到老年代的平均大小的经验值,在进行比较时候都会和老年代最大连续空间进行比较。 难道不连续只要凑起来够就不行了?毕竟老年代还是 标记-清理-(压缩) 算法。
参考 https://gist.github.com/arrayadd/0ff0a468f1e201422d5264ac350f9ab1
确实不行,因为:
- 新生代的复制算法机制决定了需要连续空间。因为分配担保机制进行时候,还没有发生Minor GC,这时候经过上次复制清理出来的空间,虽然分配了新对象,但这些对象很大程度上是连续的。一旦担保成功,显然直接复制过去最快速。
- JVM中对象的分配和回收是非常高频事情,直接决定其性能。如果为了追求理论上的合理性,来为每一个新生代晋升对象,在老年带零碎的空间中寻找能放得下空间,将会是一件非常耗时繁琐事情,况且同样是一块1Mb大小空间,到底是放一个0.8Mb的对象,还是继续遍历完所有晋升对象,比较后只为找一个更接近1Mb的。所以效率上是不允许。
一定有效吗?
如果 最大连续空闲空间大于历次晋升的平均大小 ,但是这次存活的对象突增,老年代放不下 只好在失败后进行一次 full gc
并不能减少 full gc ,该来的终归会来的
代码实验
public class PromotionTest {
private static final Integer _1MB = 1024 * 1024;
/**
* -XX:+UseSerialGC -Xmx15M -Xms15M -Xmn10M -XX:+PrintGCDetails
* 每个对象 2M,minorgc 后 survivor放不下 肯定会晋升到老年代,不必等待年龄 方便实验
* @param args
*/
public static void main(String[] args) {
byte[] a1, a2, a3, a4, a5, a6, a7, a8;
System.out.println(" -----create a1------ ");
a1 = new byte[2 * _1MB];
System.out.println(" -----create a2------ ");
a2 = new byte[2 * _1MB];
System.out.println(" -----create a3------ ");
a3 = new byte[2 * _1MB];
System.out.println(" -----clear a1 ------ ");
a1 = null;
System.out.println(" -----create a4------ ");
a4 = new byte[2 * _1MB];
System.out.println(" -----create a5------ ");
a5 = new byte[2 * _1MB];
System.out.println(" -----create a6------ ");
a6 = new byte[2 * _1MB];
System.out.println(" -----clear a4 ------ ");
a4 = null;
System.out.println(" -----clear a5 ------ ");
a5 = null;
System.out.println(" -----clear a6 ------ ");
a6 = null;
System.out.println(" -----create a7------ ");
a7 = new byte[2 * _1MB];
}
}
gc日志分析
eden区8m,创建了3个2m对象后 占用的内存超过6m,因为每个对象本身 还有对象头等需要占用一点内存;所以当要创建第4个对象的时候 eden就放不下 需要进行 minor gc了
执行日志
-----create a1------
-----create a2------
-----create a3------
[GC (Allocation Failure) [DefNew: 6325K->543K(9216K), 0.0035420 secs] 6325K->4639K(15360K), 0.0035620 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
第一次 分配失败
当要创建 a4的时候,进行 minor gc, 回收掉 a1,由于survivor放不下a2和a3,直接进入老年代; 此时:年轻代: 空, 老年代: a2, a3
9216K = eden + suvivor * 1
继续创建 a4、a5、a6,都在年轻代中
-----clear a1 ------
-----create a4------
-----create a5------
-----create a6------
[GC (Allocation Failure) [DefNew: 6924K->6924K(9216K), 0.0000118 secs][Tenured: 4096K->4097K(6144K), 0.0023158 secs] 11020K->8654K(15360K), [Metaspace: 3026K->3026K(1056768K)], 0.0023555 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
**第二次 分配失败
*创建 a7 的时候,eden放不下了,需要先进行gc ,但是由于“分配担保”机制
老年代目前 5m - 2m2 放不下年轻代所有对象,也放不下之前晋升的 a2+a3的空间大小,担保失败了; jdk1.6 update24之后 handlePromotionFailure失效了,所以这边故意把老年代设置成比较小的 5m
-----clear a4 ------
-----clear a5 ------
-----clear a6 ------
-----create a7------
[Full GC (Allocation Failure) [Tenured: 4097K->4549K(6144K), 0.0016231 secs] 10995K->4549K(15360K), [Metaspace: 3034K->3034K(1056768K)], 0.0016412 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
分配担保失败,进行 full gc
老年代里边的 a2 和 a3 还存活 内存不变
年轻代的 a4、a5、a6 无引用了,回收掉;
Heap
def new generation total 9216K, used 2138K [0x00000007bf000000, 0x00000007bfa00000, 0x00000007bfa00000)
eden space 8192K, 26% used [0x00000007bf000000, 0x00000007bf216b60, 0x00000007bf800000)
from space 1024K, 0% used [0x00000007bf900000, 0x00000007bf900000, 0x00000007bfa00000)
to space 1024K, 0% used [0x00000007bf800000, 0x00000007bf800000, 0x00000007bf900000)
tenured generation total 6144K, used 4549K [0x00000007bfa00000, 0x00000007c0000000, 0x00000007c0000000)
the space 6144K, 74% used [0x00000007bfa00000, 0x00000007bfe717e0, 0x00000007bfe71800, 0x00000007c0000000)
Metaspace used 3050K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 333K, capacity 388K, committed 512K, reserved 1048576K
去掉分配担保机制可以吗?
从分配担保的逻辑中,可以看出 分配担保最关键作用是,在进行Minor GC前是否需要根据历届晋升到老年代的平均值来发起一次Full GC. 换句话来说,去掉分配担保(也就是相当于参数HandlePromotionFailure=false)就意味着Full GC发生的几率更大。
结论
分配担保机制可以不要,但会导致Full GC更容易发生,进而导致所谓的Stop The World,虚拟机短暂停止,吞吐量,性能下降。 有了分配担保机制,就可以借鉴经验值来减少Full GC这种耗时降低性能行为。