GC悲观策略之Serial GC篇

仍然是http://blog.bluedavy.com/?p=166这篇blog中的代码,换成-Xms30m -Xmx30m -Xmn10m -XX:+UseSerialGC后执行的结果为YGC、YGC、YGC、YGC、FGC。
原因就在于Serial GC的悲观策略是不同的,下面就来分析下。

仍然是http://bluedavy.me/?p=166这篇blog中的代码,换成-Xms30m -Xmx30m -Xmn10m -XX:+UseSerialGC后执行的结果为YGC、YGC、YGC、YGC、FGC。
原因就在于Serial GC的悲观策略是不同的,Serial GC在执行YGC时,首先进入如下代码片段进行检查:
[c]
void DefNewGeneration::collect(bool full,
bool clear_all_soft_refs,
size_t size,
bool is_tlab) {

if (!collection_attempt_is_safe()) {
gch->set_incremental_collection_will_fail();
return;
}

}
bool DefNewGeneration::collection_attempt_is_safe() {
if (!to()->is_empty()) {
return false;
}
if (_next_gen == NULL) {
GenCollectedHeap* gch = GenCollectedHeap::heap();
_next_gen = gch->next_gen(this);
assert(_next_gen != NULL,
“This must be the youngest gen, and not the only gen”);
}

const double evacuation_ratio = MaxLiveObjectEvacuationRatio / 100.0;

size_t worst_case_evacuation = (size_t)(used() * evacuation_ratio);
// 这里的_next_gen也就是旧生代了,下面贴出旧生代对应的代码
return _next_gen->promotion_attempt_is_safe(worst_case_evacuation,
HandlePromotionFailure);
}
bool TenuredGeneration::promotion_attempt_is_safe(
size_t max_promotion_in_bytes,
bool younger_handles_promotion_failure) const {
bool result = max_contiguous_available() >= max_promotion_in_bytes;
if (younger_handles_promotion_failure && !result) {
result = max_contiguous_available() >=
(size_t) gc_stats()->avg_promoted()->padded_average();
if (PrintGC && Verbose && result) {
gclog_or_tty->print_cr(“TenuredGeneration::promotion_attempt_is_safe”
” contiguous_available: ” SIZE_FORMAT
” avg_promoted: ” SIZE_FORMAT,
max_contiguous_available(),
gc_stats()->avg_promoted()->padded_average());
}
} else {
if (PrintGC && Verbose) {
gclog_or_tty->print_cr(“TenuredGeneration::promotion_attempt_is_safe”
” contiguous_available: ” SIZE_FORMAT
” promotion_in_bytes: ” SIZE_FORMAT,
max_contiguous_available(), max_promotion_in_bytes);
}
}
return result;
}
[/c]
这个检查首先是检查目前新生代中使用的空间是否大于了旧生代剩余的空间,如大于且HandlePromotionFailure为true(默认值),那么再检查旧生代剩余的空间是否大于之前平均晋升的old的大小,如大于则返回true,小于则返回false,在返回false的情况下,就不进行YGC的剩下的操作了。
按照这样的规则,在第九次循环的时候,应该执行的是FGC,而不是YGC,这里的原因是在Serial GC时,是先进行计数和时间的统计等,再调用DefNewGeneration的collect的,因此尽管这次没有真正的执行YGC的动作,但还是被计数和计入时间了,但这次为什么GC log中输出的不是Full GC呢,请看下面的代码片段:
[c]
void GenCollectedHeap::do_collection(bool full,
bool clear_all_soft_refs,
size_t size,
bool is_tlab,
int max_level) {

// 在当前场景下,传入的full为false,因此complete为false
bool complete = full && (max_level == (n_gens()-1));
const char* gc_cause_str = “GC “;
if (complete) {
GCCause::Cause cause = gc_cause();
if (cause == GCCause::_java_lang_system_gc) {
gc_cause_str = “Full GC (System) “;
} else {
gc_cause_str = “Full GC “;
}
}

for (int i = starting_level; i <= max_level; i++) { if (_gens[i]->should_collect(full, size, is_tlab)) {

// Determine if allocation request was met.
if (size > 0) {
if (!is_tlab || _gens[i]->supports_tlab_allocation()) {
if (size*HeapWordSize <= _gens[i]->unsafe_max_alloc_nogc()) {
size = 0;
}
}
}

}
}
[/c]
从上可看出,当YGC结束后,eden的空间可以满足分配的需求的话,需要分配的对象的大小size就被置为零了,而在第九次循环中,由于YGC提前结束,因此eden的空间是仍然不足的,此时需要分配的size大小会不变,上面的GC动作还将进入TenuredGeneration的should_allocate来进行检查了,该方法的代码片段如下:
[c]
bool TenuredGeneration::should_collect(bool full,
size_t size,
bool is_tlab) {
// This should be one big conditional or (||), but I want to be able to tell
// why it returns what it returns (without re-evaluating the conditionals
// in case they aren’t idempotent), so I’m doing it this way.
// DeMorgan says it’s okay.
bool result = false;
// 因为full是false,因此进入不了这里
if (!result && full) {
result = true;
if (PrintGC && Verbose) {
gclog_or_tty->print_cr(“TenuredGeneration::should_collect: because”
” full”);
}
}
if (!result && should_allocate(size, is_tlab)) {
result = true;
if (PrintGC && Verbose) {
gclog_or_tty->print_cr(“TenuredGeneration::should_collect: because”
” should_allocate(” SIZE_FORMAT “)”,
size);
}
}
// If we don’t have very much free space.
// XXX: 10000 should be a percentage of the capacity!!!
if (!result && free() < 10000) { result = true; if (PrintGC && Verbose) { gclog_or_tty->print_cr(“TenuredGeneration::should_collect: because”
” free(): ” SIZE_FORMAT,
free());
}
}
// If we had to expand to accomodate promotions from younger generations
if (!result && _capacity_at_prologue < capacity()) { result = true; if (PrintGC && Verbose) { gclog_or_tty->print_cr(“TenuredGeneration::should_collect: because”
“_capacity_at_prologue: ” SIZE_FORMAT ” < capacity(): " SIZE_FORMAT, _capacity_at_prologue, capacity()); } } return result; } virtual bool should_allocate(size_t word_size, bool is_tlab) { bool result = false; // 32 bit上BitsPerSize_t为32,64 bit上为64, LogHeapWordSize在32 bit为2,64 bit为3 size_t overflow_limit = (size_t)1 << (BitsPerSize_t - LogHeapWordSize); if (!is_tlab || supports_tlab_allocation()) { result = (word_size > 0) && (word_size < overflow_limit); } return result; } [/c] 由此可看出,should_allocate为true,因此触发了FGC。 这样就可以理解为什么在第九次循环的时候打印出来的日志是没有Full GC字样的,但又计算为执行了一次YGC和一次FGC的。 由于Concurrent GC是基于Serial GC实现的,因此悲观策略是相同的。 ps: 如大家想研究这些东西,一方面是下载源码,另一方面也可以下载一个debug版本的jdk,这样就可以通过打开一些日志,看到更多的hotspot运行的细节,另外,也可以看出,Parallel GC的实现在代码上就清晰多了。

GC悲观策略之Parallel GC篇

先来看段代码:
[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]); } } } [/java] 当用-Xms30m -Xmx30m -Xmn10m -XX:+UseParallelGC执行上面的代码时会执行几次Minor GC和几次Full GC呢?

先来看段代码:
[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); } } [/java] 当用-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,代码片段如下: [c] 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; } [/c] 在上面should_attempt_scavenge代码片段中,可以看到会比较之前YGC晋升到Old中的平均大小与当前新生代中已被使用的字节数大小,取更小的值与旧生代目前剩余空间大小对比,如更大,则返回false,就终止了YGC的执行了,当返回false时,PSScavenge::invoke就将触发Full GC了。 在PSScavenge:invoke中还有一个条件为:policy->should_full_GC(heap->old_gen()->free_in_bytes(),来看看这段代码片段: [c] 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; } [/c] 可看到,这段代码检查的也是之前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执行的状况。 埋个伏笔,大家将上面的执行参数换为-XX:+UseSerialGC执行看看,会发生什么呢? 🙂