JVM 源碼解讀之 CMS 何時會進行 Full GC

簡書 滌生。
轉載請注明原創出處,謝謝!
如果讀完覺得有收獲的話,歡迎點贊加關注。

前言

本文內容是基于 JDK 8

在文章 JVM 源碼解讀之 CMS GC 觸發條件 中分析了 CMS GC 觸發的五類情況,并且提到 CMS GC 分為 foreground collector 和 background collector。
不管是 foreground collector 還是 background collector 使用的都是 mark-sweep 算法,分階段進行標記清理,優點很明顯-低延時,但最大的缺點是存在碎片,內存空間利用率低。因此,CMS 為了解決這個問題,在每次進行 foreground collector 之前,判斷是否需要進行一次壓縮式 GC。

此壓縮式 GC,CMS 使用的是跟 Serial Old GC 一樣的 LISP2 算法,其使用 mark-compact 來做 Full GC,一般稱之為 MSC(mark-sweep-compact),它收集的范圍是 Java 堆的 Young Gen 和 Old Gen,以及 metaspace(元空間)。

本文不涉及具體的收集過程,只分析 CMS 在什么情況下會進行 compact 的 Full GC。

什么情況下會進行一次壓縮式 Full GC 呢?

何時會進行 FullGC?

下面這段代碼就是 CMS 進行判斷是進行 mark-sweep 的 foreground collector,還是進行 mark-sweep-compact 的 Full GC。主要的判斷依據就是是否進行壓縮,即代碼中的 should_compact。

// Check if we need to do a compaction, or if not, whether
// we need to start the mark-sweep from scratch.
bool should_compact    = false;
bool should_start_over = false;
decide_foreground_collection_type(clear_all_soft_refs,
    &should_compact, &should_start_over);
...
if (should_compact) {
    ...
    // mark-sweep-compact
    do_compaction_work(clear_all_soft_refs);
    ...
} else {
    // mark-sweep
    do_mark_sweep_work(clear_all_soft_refs, first_state,
      should_start_over);
}

接下來我們就來分析下在什么情況下會進行 compact,
來看 decide_foreground_collection_type 函數,主要分為 4 種情況:

  1. GC(包含 foreground collector 和 compact 的 Full GC)次數
  2. GCCause 是否是用戶請求式觸發導致的
  3. 增量 GC 是否可能會失敗(悲觀策略)
  4. 是否清理所有 SoftReference
void CMSCollector::decide_foreground_collection_type(
  bool clear_all_soft_refs, bool* should_compact,
  bool* should_start_over) {
  ...
  
  // 判斷是否壓縮的邏輯
  
  *should_compact =
    UseCMSCompactAtFullCollection &&
    ((_full_gcs_since_conc_gc >= CMSFullGCsBeforeCompaction) ||
     GCCause::is_user_requested_gc(gch->gc_cause()) ||
     gch->incremental_collection_will_fail(true /* consult_young */));
  *should_start_over = false;
  
  if (clear_all_soft_refs && !*should_compact) {
  
    if (CMSCompactWhenClearAllSoftRefs) {
      *should_compact = true;
    } else {
    
        if (_collectorState > FinalMarking) {
        _collectorState = Resetting; // skip to reset to start new cycle
        reset(false /* == !asynch */);
        *should_start_over = true;
      } 
    }
  }
}

接下來我們具體看每種情況

1. GC(包含 foreground collector 和 compact 的 Full GC)次數

// UseCMSCompactAtFullCollection 參數值默認是 true
UseCMSCompactAtFullCollection &&
    ((_full_gcs_since_conc_gc >= CMSFullGCsBeforeCompaction)

這里說的 GC 次數 _full_gcs_since_conc_gc,指的是從上次 background collector 后,foreground collector 和 compact 的 Full GC 的次數,只要次數大于等于 CMSFullGCsBeforeCompaction 參數閾值,就表示可以進行一次壓縮式的 Full GC。
(CMSFullGCsBeforeCompaction 參數默認是 0,意味著默認是要進行壓縮式的 Full GC。)

2. GCCause 是否是用戶請求式觸發導致

 inline static bool is_user_requested_gc(GCCause::Cause cause) {
    return (cause == GCCause::_java_lang_system_gc ||
            cause == GCCause::_jvmti_force_gc);
  }

用戶請求式觸發導致的 GCCause 指的是 _java_lang_system_gc(即 System.gc())或者 _jvmti_force_gc(即 JVMTI 方式的強制 GC)
意味著只要是 System.gc(前提沒有配置 ExplicitGCInvokesConcurrent 參數)調用或者 JVMTI 方式的強制 GC 都會進行一次壓縮式的 Full GC。

3. 增量 GC 是否可能會失?。ū^策略)

  bool incremental_collection_will_fail(bool consult_young) {
    // Assumes a 2-generation system; the first disjunct remembers if an
    // incremental collection failed, even when we thought (second disjunct)
    // that it would not.
    assert(heap()->collector_policy()->is_two_generation_policy(),
           "the following definition may not be suitable for an n(>2)-generation system");
    return incremental_collection_failed() ||
           (consult_young && !get_gen(0)->collection_attempt_is_safe());
  }

JVM 源碼解讀之 CMS GC 觸發條件 文章中也提到了這塊內容,
指的是兩代的 GC 體系中,主要指的是 Young GC 是否會失敗。如果 Young GC 已經失敗或者可能會失敗,CMS 就認為可能存在碎片導致的,需要進行一次壓縮式的 Full GC。

“incremental_collection_failed()” 這里指的是 Young GC 已經失敗,至于為什么會失敗一般是因為 Old Gen 沒有足夠的空間來容納晉升的對象,比如常見的 “promotion failed” 。

“!get_gen(0)->collection_attempt_is_safe()” 指的是 Young Gen 存活對象晉升是否可能會失敗。
通過判斷當前 Old Gen 剩余的空間大小是否足夠容納 Young GC 晉升的對象大小。
Young GC 到底要晉升多少是無法提前知道的,因此,這里通過統計平均每次 Young GC 晉升的大小和當前 Young GC 可能晉生的最大大小來進行比較。

下面展示的就是 collection_attempt_is_safe 函數的代碼:

bool DefNewGeneration::collection_attempt_is_safe() {
  if (!to()->is_empty()) {
    if (Verbose && PrintGCDetails) {
      gclog_or_tty->print(" :: to is not empty :: ");
    }
    return false;
  }
  if (_next_gen == NULL) {
    GenCollectedHeap* gch = GenCollectedHeap::heap();
    _next_gen = gch->next_gen(this);
  }
  return _next_gen->promotion_attempt_is_safe(used());
}

4. 是否清理所有 SoftReference

if (clear_all_soft_refs && !*should_compact) {
  
    if (CMSCompactWhenClearAllSoftRefs) {
      *should_compact = true;
    } 
    ...

SoftReference 軟引用,你應該了解它的特性,一般是在內存不夠的時候,GC 會回收相關對象內存。這里說的就是需要回收所有軟引用的情況,在配置了 CMSCompactWhenClearAllSoftRefs 參數的情況下,會進行一次壓縮式的 Full GC。

JDK 1.9 有變更:
徹底去掉了 CMS forground collector 的功能,也就是說除了 background collector,就是壓縮式的 Full GC。自然(UseCMSCompactAtFullCollection、CMSFullGCsBeforeCompaction 這兩個參數也已經不在支持了。

總結

本文著重介紹了 CMS 在以下 4 種情況:

  • GC(包含 foreground collector 和 compact 的 Full GC)次數
  • GCCause 是否是用戶請求式觸發導致
  • 增量 GC 是否可能會失?。ū^策略)
  • 是否清理所有 SoftReference

會進行壓縮式的 Full GC,并且詳細介紹了每種情況下的觸發條件。
我們在 GC 調優時應該盡可能的避免壓縮式的 Full GC,因為其使用的是 Serial Old GC 類似算法,它是單線程對全堆以及 metaspace 進行回收,STW 的時間會特別長,對業務系統的可用性影響比較大。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。