先来看段代码: Java代码 import java.util.*; public class SummaryCase{ public static void main(String[] args) throws Exception{ List caches=new ArrayList(); for(int i=0;i<7;i++){ caches.add(new byte[1024*1024*3]); } caches.clear(); for(int i=0;i<2;i++){ caches.add(new byte[1024*1024*3]); } Thread.sleep(10000); } } 当用-Xms30m -Xmx30m -Xmn10m -XX:+UseParallelGC 执行上面的代码时会执行几次Minor GC和几次Full GC呢? 按照eden空间不足时触发minor gc的规则,上面代码执行后的GC应为:M、M、M、M ,但实际上上面代码执行后GC则为:M、M、M、F、F 。 这里的原因就在于Parallel Scavenge GC时的悲观策略,当在eden上分配内存失败时且对象的大小尚不需要直接在old上分配时,会触发YGC,代码片段如下: Cpp代码 void PSScavenge::invoke(){ ... bool scavenge_was_done = PSScavenge::invoke_no_policy(); PSGCAdaptivePolicyCounters* counters = heap->gc_policy_counters(); if (UsePerfData) counters->update_full_follows_scavenge(0); if(!scavenge_was_done || policy->should_full_GC(heap->old_gen()->free_in_bytes())){ if(UsePerfData) counters->update_full_follows_scavenge(full_follows_scavenge); < GCCauseSetter gccs(heap, GCCause::_adaptive_size_policy); if (UseParallelOldGC){ PSParallelCompact::invoke_no_policy(false); }else{ PSMarkSweep::invoke_no_policy(false); } } ... } PSScavenge::invoke_no_policy{ ... if(!should_attempt_scavenge()){ return false; } ... } bool PSScavenge::should_attempt_scavenge(){ ... PSAdaptiveSizePolicy* policy = heap->size_policy(); size_t avg_promoted = (size_t) policy->padded_average_promoted_in_bytes(); size_t promotion_estimate = MIN2(avg_promoted, young_gen->used_in_bytes()); bool result = promotion_estimate < old_gen->free_in_bytes(); ... return result; } 在上面should_attempt_scavenge代码片段中,可以看到会比较之前YGC晋升到Old中的平均大小与当前新生代中已被使用的字节数大小,取更小的值与旧生代目前剩余空间大小对比,如更大,则返回false,就终止了YGC的执行了,当返回false时,PSScavenge::invoke就将触发Full GC了。 在PSScavenge:invoke中还有一个条件为:policy->should_full_GC(heap->old_gen()->free_in_bytes(),来看看这段代码片段: Cpp代码 bool PSAdaptiveSizePolicy::should_full_GC(size_t old_free_in_bytes){ bool result = padded_average_promoted_in_bytes() > (float) old_free_in_bytes; ... return result; } 可看到,这段代码检查的也是之前YGC时晋升到old的平均大小是否大于了旧生代的剩余空间,如大于,则触发full gc。 总结上面分析的策略,可以看到采用Parallel GC的情况下,当YGC触发时,会有两个检查: 1、在YGC执行前,min(目前新生代已使用的大小,之前平均晋升到old的大小中的较小值) > 旧生代剩余空间大小 不执行YGC,直接执行Full GC : 执行YGC; 2、在YGC执行后,平均晋升到old的大小 > 旧生代剩余空间大小 触发Full GC : 什么都不做。 按照这样的说明,再来看看上面代码的执行过程中eden和old大小的变化状况:
代码
Eden
Old
YGC
FGC
第一次循环
3
0
0
0
第二次循环
6
0
0
0
第三次循环
3
6
1
0
第四次循环
6
6
1
0
第五次循环
3
12
2
0
第六次循环
6
12
2
0
第七次循环
3
18
3
1
第八次循环
6
18
3
1
第九次循环
3
3
3
2
在第7次循环时,YGC后旧生代剩余空间为2m,而之前平均晋级到old的对象大小为6m,因此在YGC后会触发一次FGC。 而第9次循环时,在YGC执行前,此时新生代已使用的大小为6m,之前晋级到old的平均大小为6m,这两者去最小值为6m,这个值已大于old的剩余空间,因此就不执行YGC,直接执行FGC了。
Sun JDK之所以要有悲观策略,我猜想理由是程序最终是会以一个较为稳态的状况执行的,此时每次YGC后晋升到old的对象大小应该是差不多的,在YGC时做好检查,避免等YGC后晋升到Old的对象导致old空间不足,因此还不如干脆就直接执行FGC,正因为悲观策略的存在,大家有些时候可能会看到old空间没满但full gc执行的状况。
作者“lewis@Taobao ”