首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

GC 悲觀策略深度解析:Serial GC 為何提前觸發 Full GC(含源碼級證據)

     編輯:Java 相關知識

對比 -XX:+UseSerialGC 與 -XX:+UseParallelGC 在相同堆配置(-Xms30m -Xmx30m -Xmn10m)下運行同樣分配壓力代碼時的 GC 行為差異,Serial GC 出現 YGC、YGC、YGC、YGC、FGC 模式(第 9 次本應 YGC 卻觸發 FGC),深度剖析 Serial GC 悲觀策略源碼邏輯,揭示「預防性 Full GC」的觸發條件與 GC 日誌輸出機制差異。

測試代碼與現象


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);
    }
}

  • Parallel GC:M、M、M、M(4 次 Minor GC)
  • Serial GC:YGC、YGC、YGC、YGC、FGC(第 9 次循環觸發 Full GC)

Serial GC 悲觀策略源碼鏈

1. 新生代收集前檢查:DefNewGeneration::collect()


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;  // 直接返回,不執行 YGC
    }
    ...
}

2. 安全性判斷核心:DefNewGeneration::collection_attempt_is_safe()


bool DefNewGeneration::collection_attempt_is_safe() {
    if (!to()->is_empty()) return false;  // Survivor 區非空直接判定不安全
    
    if (_next_gen == NULL) {  // 獲取老年代引用
        _next_gen = GenCollectedHeap::heap()->next_gen(this);
    }
    
    const double evacuation_ratio = MaxLiveObjectEvacuationRatio / 100.0;
    size_t worst_case_evacuation = (size_t)(used() * evacuation_ratio);
    
    // 關鍵:委託老年代判斷能否承接最壞情況下的晉升對象
    return _next_gen->promotion_attempt_is_safe(worst_case_evacuation, HandlePromotionFailure);
}

3. 老年代承接能力檢查:TenuredGeneration::promotion_attempt_is_safe()


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: %zu avg_promoted: %zu",
                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: %zu promotion_in_bytes: %zu",
                max_contiguous_available(), max_promotion_in_bytes);
        }
    }
    return result;
}

關鍵邏輯解讀

  1. 最壞情況估算:新生代已用空間 × MaxLiveObjectEvacuationRatio(默認 100%)= 理論最大晉升量。
  2. 連續可用空間判斷:老年代最大連續可用空間 ≥ 估算晉升量?
  3. 降級策略:若不滿足且 HandlePromotionFailure=true(默認),改用歷史平均晉升量判斷。
  4. 判定失敗 → 直接放棄 YGC:collect() 早期返回,但統計計數器已遞增(GC 日誌仍記為 YGC)。

為何 GC Log 顯示 YGC 而非 Full GC?

在 GenCollectedHeap::do_collection() 中:


bool complete = full && (max_level == (n_gens()-1));  // 傳入 full=false → complete=false
const char* gc_cause_str = "GC ";  // 非 complete 標記為普通 GC
if (complete) { ... }  // 只有 complete=true 才標記 Full GC

// YGC 結束後若 Eden 可滿足分配,size=0
// 第 9 次循環:YGC 提前退出,Eden 仍不足,size 保持 >0
// 進入 TenuredGeneration::should_collect() → should_allocate() → 觸發 Full GC

結論:第 9 次循環實際執行了 Full GC,但因 do_collection 入口參數 full=false,日誌標記為普通 GC(YGC),下一輪分配失敗才真正進入 Full GC 入口並標記為 Full GC。

對比啟示

  • Serial GC 悲觀策略更激進:基於最壞情況估算 + 連續空間檢查,寧可提前 Full GC 也不冒險 YGC 失敗。
  • Parallel GC 悲觀策略(下文分析)基於歷史平均晉升量,相對保守。
  • 理解悲觀策略可解釋「老年代未滿卻觸發 Full GC」的常見現象。
 

換成-Xms30m -Xmx30m -Xmn10m -XX:+UseSerialGC後執行的結果為YGC、YGC、YGC、YGC、FGC。
原因就在於Serial GC的悲觀策略是不同的,Serial GC在執行YGC時,首先進入如下代碼片段進行檢查:
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;
}


這個檢查首先是檢查目前新生代中使用的空間是否大於了舊生代剩余的空間,如大於且HandlePromotionFailure為true(默認值),那麼再檢查舊生代剩余的空間是否大於之前平均晉升的old的大小,如大於則返回true,小於則返回false,在返回false的情況下,就不進行YGC的剩下的操作了。
按照這樣的規則,在第九次循環的時候,應該執行的是FGC,而不是YGC,這裡的原因是在Serial GC時,是先進行計數和時間的統計等,再調用DefNewGeneration的collect的,因此盡管這次沒有真正的執行YGC的動作,但還是被計數和計入時間了,但這次為什麼GC log中輸出的不是Full GC呢,請看下面的代碼片段:
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;

}
}
}
...
}
}


從上可看出,當YGC結束後,eden的空間可以滿足分配的需求的話,需要分配的對象的大小size就被置為零了,而在第九次循環中,由於YGC提前結束,因此eden的空間是仍然不足的,此時需要分配的size大小會不變,上面的GC動作還將進入TenuredGeneration的should_allocate來進行檢查了,該方法的代碼片段如下:
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 ")",  

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved