《深入理解java虚拟机v3》 3.8.5 空间分配担保 代码清单3-11

1. 原文

在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。

如果不成立,则虚拟机会先查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败(Handle Promotion Failure);如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者-XX:HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次Full GC

解释一下“冒险”是冒了什么风险:前面提到过,新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况
——最极端的情况就是内存回收后新生代中所有对象都存活,需要老年代进行分配担保,把Survivor无法容纳的对象直接送入老年代,这与生活中贷款担保类似。老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,但一共有多少对象会在这次回收中活下来在实际完成内存回收之前是无法明确知道的,所以只能取之前每一次回收晋升到老年代对象容量的平均大小作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。

举例:
假设新生代内存是8M,已占用6M,剩余2M,Survivor是1M,未占用,老年代是10M,已经占用8M,此时老年代剩余2M。

此时,需要放入一个3M的对象,新生代剩余空间不够(此时仅剩2M),需要触发Minor GC,那么我们知道,如果发生Minor GC,剩余存活的需要复制到老年代(假设存活15次,符合转移条件)。

假设此时新生代的6M都存活了,都需要放入老年代,此时,老年代仅剩3M,再放入6M,就触发Full GC了;假设此时新生代只有1M存活了,那么老年代就能放的下,不会发生Full GC。

问题来了,在真正执行Minor GC之前,谁也不能预制到有多少对象需要转移到老年代,也无法预料老年代会不会发生Full GC。因此在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,假设场景A满足条件,场景B不满足。

  • 场景A
    放心的执行Minor GC,反正老年代剩余空间足够大,可以兜底。
  • 场景B
    产生2种可能,场景B1实际会产生Full GC,场景B2不会发生Full GC。
    也就是说场景B也不一定非要发生Full GC,如果每次都强制执行full gc,那么效率太低。那么如果大概率不发生Full GC,那么没必要每次都去执行Full GC,毕竟比较耗时。这个概率怎么算呢,就是文中的历史平均值了。

为什么当老年代剩余空间不够当前新生代的空间(假设一定触发full gc的情况下),优先 full gc,再minor gc,而不是先minor gc,再full gc呢? 原因是如果是后者,那么效率低。如果先 full gc,后minor gc效率高,full gc会清理大量的对年轻带的引用,这样minor gc可以清掉大量对象。

取历史平均值来比较其实仍然是一种赌概率的解决办法,也就是说假如某次Minor GC存活后的对象突增,远远高于历史平均值的话,依然会导致担保失败。如果出现了担保失败,那就只好老老实实地重新发起一次Full GC,这样停顿时间就很长了。虽然担保失败时绕的圈子是最大的,但通常情况下都还是会将-XX:HandlePromotionFailure开关打开,避免Full GC过于频繁。

参见代码清单3-11,请读者先以JDK 6 Update 24之前的HotSpot运行测试代码。

代码中是JDK1.6 24后的版本执行过程的注释,等价于开启参数-XX:HandlePromotionFailure=true

private static final int _1MB = 1024 * 1024;

/**
 * -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
 * -XX:-HandlePromotionFailure  -XX:+UseSerialGC
 */
public static void testHandlePromotion() {
	byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7;
	allocation1 = new byte[2 * _1MB]; // 进入Eden区
	allocation2 = new byte[2 * _1MB]; // 进入Eden区
	allocation3 = new byte[2 * _1MB]; // 进入Eden区
	allocation1 = null;
	// 由于Eden区最大支持8M,已经6M,不够再放入2M, 触发minor gc,因为老年代可用空间10M>6M,担保成功
	// gc后,allocation1销毁,allocation2、allocation3进入老年代,共占用4M,老年代剩余6M
	// allocation5 进入Eden区
	allocation4 = new byte[2 * _1MB];

	allocation5 = new byte[2 * _1MB]; // 进入Eden区,此时共占用4M

	allocation6 = new byte[2 * _1MB]; // 进入Eden区,此时共占用6M
	allocation4 = null;
	allocation5 = null;
	allocation6 = null;
	// eden区已经占用6M,再次触发minor gc
	// 因为老年代剩余6M,虽然不够放入整个eden区+survivor(6M+148K),但是大于历史进入的值4M
	//gc后,allocation4、allocation5、allocation6被删除,allocation7进入Eden区
	allocation7 = new byte[2 * _1MB];
}

public static void main(String[] args) {
	testHandlePromotion();
}

以-XX:HandlePromotionFailure=false参数来运行的结果,没有24之前的版本,直接拷贝书上的:

[GC [DefNew: 6651K->148K(9216K), 0.0078936 secs] 6651K->4244K(19456K), 0.0079192 secs] [Times: user=0.00 sys=0.02, real=0.02 secs]
[GC [DefNew: 6378K->6378K(9216K), 0.0000206 secs][`Tenured`: 4096K->4244K(10240K), 0.0042901 secs] 10474K->4   `//full gc`

可以看到第二次gc是full gc

以-XX:HandlePromotionFailure=true参数来运行的结果:

[`GC`[`DefNew`: 6487K->147K(9216K), 0.0050823 secs] 6487K->4243K(19456K), 0.0051264 secs] [Times: user=0.00 sys=0.02, real=0.02 secs]   `//minor gc`
[`GC` [`DefNew`: 6377K->147K(9216K), 0.0013094 secs] 10473K->4243K(19456K), 0.0013778 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]   `//minor gc`
Heap
 def new generation   total 9216K, used 2359K [0x32360000, 0x32d60000, 0x32d60000)
  eden space 8192K,  27% used [0x32360000, 0x32588fe0, 0x32b60000)    `allocation7进入Eden区`
  from space 1024K,  `14% used` [0x32b60000, 0x32b84d70, 0x32c60000)   `survivor 148K`
  to   space 1024K,   0% used [0x32c60000, 0x32c60000, 0x32d60000)
 tenured generation   total 10240K, used 4096K [0x32d60000, 0x33760000, 0x33760000)
   the space 10240K,  40% used [0x32d60000, 0x33160020, 0x33160200, 0x33760000)   `allocation2、allocation3共4M`
 compacting perm gen  total 12288K, used 380K [0x33760000, 0x34360000, 0x37760000)
   the space 12288K,   3% used [0x33760000, 0x337bf220, 0x337bf400, 0x34360000)
    ro space 10240K,  51% used [0x37760000, 0x37c925d0, 0x37c92600, 0x38160000)
    rw space 12288K,  55% used [0x38160000, 0x387fd978, 0x387fda00, 0x38d60000)
Warning: The flag -HandlePromotionFailure has been EOL'd as of 6.0_24 and will be ignored

在JDK 6 Update 24之后,这个测试结果就有了差异,-XX:HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,观察OpenJDK中的源码变化(见代码清单3-12),虽然源码中还定义了-XX:HandlePromotionFailure参数,但是在实际虚拟机中已经不会再使用它。JDK 6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行Minor GC,否则将进行Full GC。

等同于在JDK 6 Update 24之后,默认设置-XX:HandlePromotionFailure=true

2. 图示

2.1 JDK 6 Update 24之前

在发生Minor GC之前,虚拟机必须先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那这一次Minor GC可以确保是安全的。如果不成立,则虚拟机会先查看-XX:HandlePromotionFailure参数的设置值是否允许担保失败(HandlePromotion Failure);如果允许,那会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者-XX:HandlePromotionFailure设置不允许冒险,那这时就要改为进行一次FullGC。

在这里插入图片描述
在JDK 6 Update 24之后:

  • (1)JDK 6 Update 24之后不再使用-XX:HandlePromotionFailure参数。
  • (2)JDK 6 Update 24之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小,就会进行Minor GC,否则将进行Full GC。
    在这里插入图片描述




参考:
《空间分配担保机制》

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值