JVM 源碼解讀之 CMS GC 觸發(fā)條件

簡書 滌生朴下。
轉(zhuǎn)載請(qǐng)注明原創(chuàng)出處镜雨,謝謝!
如果讀完覺得有收獲的話恃疯,歡迎點(diǎn)贊加關(guān)注漏设。

前言

經(jīng)常有同學(xué)會(huì)問,為啥我的應(yīng)用 Old Gen 沒到 CMSInitiatingOccupancyFraction 參數(shù)配置的閾值今妄,就觸發(fā)了 CMS GC郑口,表示很莫名奇妙,不知道問題出在哪盾鳞?

其實(shí) CMS GC 的觸發(fā)條件非常多犬性,不只是 CMSInitiatingOccupancyFraction 閾值觸發(fā)這么簡單。本文通過源碼全面梳理了觸發(fā) CMS GC 的條件腾仅,盡可能的幫你了解平時(shí)遇到的奇奇怪怪的 CMS GC 問題乒裆。

先拋出一些問題,來吸引你的注意力推励。

為什么 Old Gen 使用占比僅 50% 就進(jìn)行了一次 CMS GC鹤耍?
Metaspace 的使用也會(huì)觸發(fā) CMS GC 嗎肉迫?
為什么 Old Gen 使用占比非常小就進(jìn)行了一次 CMS GC?

觸發(fā)條件

CMS GC 在實(shí)現(xiàn)上分成 foreground collector 和 background collector惰蜜。foreground collector 相對(duì)比較簡單昂拂,background collector 比較復(fù)雜受神,情況比較多抛猖。

下面我們從 foreground collector 和 background collector 分別來說明他們的觸發(fā)條件:

說明:本文內(nèi)容是基于 JDK 8
說明:本文僅涉及 CMS GC 的觸發(fā)條件,至于算法的具體過程鼻听,以及什么時(shí)候進(jìn)行 MSC(mark sweep compact)不在本文范圍

foreground collector

foreground collector 觸發(fā)條件比較簡單财著,一般是遇到對(duì)象分配但空間不夠,就會(huì)直接觸發(fā) GC撑碴,來立即進(jìn)行空間回收撑教。采用的算法是 mark sweep,不壓縮醉拓。

background collector

說明 background collector 的觸發(fā)條件之前伟姐,先來說下 background collector 的流程,它是通過 CMS 后臺(tái)線程不斷的去掃描亿卤,過程中主要是判斷是否符合 background collector 的觸發(fā)條件愤兵,一旦有符合的情況,就會(huì)進(jìn)行一次 background 的 collect排吴。

void ConcurrentMarkSweepThread::run() {
  ...//省略
  while (!_should_terminate) {
    sleepBeforeNextCycle();
    if (_should_terminate) break;
    GCCause::Cause cause = _collector->_full_gc_requested ?
      _collector->_full_gc_cause : GCCause::_cms_concurrent_mark;
    _collector->collect_in_background(false, cause);
  }
  ...//省略
}

每次掃描過程中秆乳,先等 CMSWaitDuration 時(shí)間,然后再去進(jìn)行一次 shouldConcurrentCollect 判斷钻哩,看是否滿足 CMS background collector 的觸發(fā)條件屹堰。CMSWaitDuration 默認(rèn)時(shí)間是 2s(經(jīng)常會(huì)有業(yè)務(wù)遇到頻繁的 CMS GC,注意看每次 CMS GC 之間的時(shí)間間隔街氢,如果是 2s扯键,那基本就可以斷定是 CMS 的 background collector)。

void ConcurrentMarkSweepThread::sleepBeforeNextCycle() {
  while (!_should_terminate) {
    if (CMSIncrementalMode) {
      icms_wait();
      if(CMSWaitDuration >= 0) {
        // Wait until the next synchronous GC, a concurrent full gc
        // request or a timeout, whichever is earlier.
        wait_on_cms_lock_for_scavenge(CMSWaitDuration);
      }
      return;
    } else {
      if(CMSWaitDuration >= 0) {
        // Wait until the next synchronous GC, a concurrent full gc
        // request or a timeout, whichever is earlier.
        wait_on_cms_lock_for_scavenge(CMSWaitDuration);
      } else {
        // Wait until any cms_lock event or check interval not to call shouldConcurrentCollect permanently
        wait_on_cms_lock(CMSCheckInterval);
      }
    }
    // Check if we should start a CMS collection cycle
    if (_collector->shouldConcurrentCollect()) {
      return;
    }
    // .. collection criterion not yet met, let's go back
    // and wait some more
  }
}

那 shouldConcurrentCollect() 方法中都有哪些條件呢珊肃?

bool CMSCollector::shouldConcurrentCollect() {

  // 第一種觸發(fā)情況
  if (_full_gc_requested) {
    if (Verbose && PrintGCDetails) {
      gclog_or_tty->print_cr("CMSCollector: collect because of explicit "
                             " gc request (or gc_locker)");
    }
    return true;
  }
  
  // For debugging purposes, change the type of collection.
  // If the rotation is not on the concurrent collection
  // type, don't start a concurrent collection.
  NOT_PRODUCT(
    if (RotateCMSCollectionTypes &&
        (_cmsGen->debug_collection_type() !=
          ConcurrentMarkSweepGeneration::Concurrent_collection_type)) {
      assert(_cmsGen->debug_collection_type() !=
        ConcurrentMarkSweepGeneration::Unknown_collection_type,
        "Bad cms collection type");
      return false;
    }
  )
  FreelistLocker x(this);
  // ------------------------------------------------------------------
  // Print out lots of information which affects the initiation of
  // a collection.
  if (PrintCMSInitiationStatistics && stats().valid()) {
    gclog_or_tty->print("CMSCollector shouldConcurrentCollect: ");
    gclog_or_tty->stamp();
    gclog_or_tty->print_cr("");
    stats().print_on(gclog_or_tty);
    gclog_or_tty->print_cr("time_until_cms_gen_full %3.7f",
      stats().time_until_cms_gen_full());
    gclog_or_tty->print_cr("free="SIZE_FORMAT, _cmsGen->free());
    gclog_or_tty->print_cr("contiguous_available="SIZE_FORMAT,
                           _cmsGen->contiguous_available());
    gclog_or_tty->print_cr("promotion_rate=%g", stats().promotion_rate());
    gclog_or_tty->print_cr("cms_allocation_rate=%g", stats().cms_allocation_rate());
    gclog_or_tty->print_cr("occupancy=%3.7f", _cmsGen->occupancy());
    gclog_or_tty->print_cr("initiatingOccupancy=%3.7f", _cmsGen->initiating_occupancy());
    gclog_or_tty->print_cr("metadata initialized %d",
      MetaspaceGC::should_concurrent_collect());
  }
  // ------------------------------------------------------------------
  
  // 第二種觸發(fā)情況
  // If the estimated time to complete a cms collection (cms_duration())
  // is less than the estimated time remaining until the cms generation
  // is full, start a collection.
  if (!UseCMSInitiatingOccupancyOnly) {
    if (stats().valid()) {
      if (stats().time_until_cms_start() == 0.0) {
        return true;
      }
    } else {
      // We want to conservatively collect somewhat early in order
      // to try and "bootstrap" our CMS/promotion statistics;
      // this branch will not fire after the first successful CMS
      // collection because the stats should then be valid.
      if (_cmsGen->occupancy() >= _bootstrap_occupancy) {
        if (Verbose && PrintGCDetails) {
          gclog_or_tty->print_cr(
            " CMSCollector: collect for bootstrapping statistics:"
            " occupancy = %f, boot occupancy = %f", _cmsGen->occupancy(),
            _bootstrap_occupancy);
        }
        return true;
      }
    }
  }
  
  // 第三種觸發(fā)情況
  // Otherwise, we start a collection cycle if
  // old gen want a collection cycle started. Each may use
  // an appropriate criterion for making this decision.
  // XXX We need to make sure that the gen expansion
  // criterion dovetails well with this. XXX NEED TO FIX THIS
  if (_cmsGen->should_concurrent_collect()) {
    if (Verbose && PrintGCDetails) {
      gclog_or_tty->print_cr("CMS old gen initiated");
    }
    return true;
  }

  // 第四種觸發(fā)情況
  // We start a collection if we believe an incremental collection may fail;
  // this is not likely to be productive in practice because it's probably too
  // late anyway.
  GenCollectedHeap* gch = GenCollectedHeap::heap();
  assert(gch->collector_policy()->is_two_generation_policy(),
         "You may want to check the correctness of the following");
  if (gch->incremental_collection_will_fail(true /* consult_young */)) {
    if (Verbose && PrintGCDetails) {
      gclog_or_tty->print("CMSCollector: collect because incremental collection will fail ");
    }
    return true;
  }
  
  // 第五種觸發(fā)情況
  if (MetaspaceGC::should_concurrent_collect()) {
      if (Verbose && PrintGCDetails) {
      gclog_or_tty->print("CMSCollector: collect for metadata allocation ");
      }
      return true;
    }

  return false;
}

上述代碼可知荣刑,從大類上分 background collector 一共有 5 種觸發(fā)情況:

  • 是否是并行 Full GC

指的是在 GC cause 是 _gc_locker 且配置了 GCLockerInvokesConcurrent 參數(shù), 或者 GC cause 是_java_lang_system_gc(就是 System.gc()調(diào)用)and 且配置了 ExplicitGCInvokesConcurrent 參數(shù),這是會(huì)觸發(fā)一次 background collector近范。

  • 根據(jù)統(tǒng)計(jì)數(shù)據(jù)動(dòng)態(tài)計(jì)算(僅未配置 UseCMSInitiatingOccupancyOnly 時(shí))

未配置 UseCMSInitiatingOccupancyOnly 時(shí)嘶摊,會(huì)根據(jù)統(tǒng)計(jì)數(shù)據(jù)動(dòng)態(tài)判斷是否需要進(jìn)行一次 CMS GC。

判斷邏輯是评矩,如果預(yù)測 CMS GC 完成所需要的時(shí)間大于預(yù)計(jì)的老年代將要填滿的時(shí)間叶堆,則進(jìn)行 GC。
這些判斷是需要基于歷史的 CMS GC 指標(biāo)斥杜,然而虱颗,第一次 CMS GC 時(shí)沥匈,統(tǒng)計(jì)數(shù)據(jù)還沒有形成是無效的,這時(shí)會(huì)跟據(jù) Old Gen 的使用占比來進(jìn)行判斷是否要進(jìn)行 GC忘渔。

if (!UseCMSInitiatingOccupancyOnly) {
    if (stats().valid()) {
      if (stats().time_until_cms_start() == 0.0) {
        return true;
      }
    } else {
      // We want to conservatively collect somewhat early in order
      // to try and "bootstrap" our CMS/promotion statistics;
      // this branch will not fire after the first successful CMS
      // collection because the stats should then be valid.
      if (_cmsGen->occupancy() >= _bootstrap_occupancy) {
        if (Verbose && PrintGCDetails) {
          gclog_or_tty->print_cr(
            " CMSCollector: collect for bootstrapping statistics:"
            " occupancy = %f, boot occupancy = %f", _cmsGen->occupancy(),
            _bootstrap_occupancy);
        }
        return true;
      }
    }
  }

那占多少比率高帖,開始回收呢?(也就是 _bootstrap_occupancy 的值是多少呢畦粮?)
答案是 50%散址。或許你已經(jīng)遇到過類似案例宣赔,在沒有配置 UseCMSInitiatingOccupancyOnly 時(shí)预麸,發(fā)現(xiàn)老年代占比到 50% 就進(jìn)行了一次 CMS GC,當(dāng)時(shí)的你或許還一頭霧水呢儒将。

 _bootstrap_occupancy = ((double)CMSBootstrapOccupancy)/(double)100;
 //參數(shù)默認(rèn)值
 product(uintx, CMSBootstrapOccupancy, 50,
          "Percentage CMS generation occupancy at which to initiate CMS collection for bootstrapping collection stats")  
  • 根據(jù) Old Gen 情況判斷
bool ConcurrentMarkSweepGeneration::should_concurrent_collect() const {
  assert_lock_strong(freelistLock());
  if (occupancy() > initiating_occupancy()) {
    if (PrintGCDetails && Verbose) {
      gclog_or_tty->print(" %s: collect because of occupancy %f / %f  ",
        short_name(), occupancy(), initiating_occupancy());
    }
    return true;
  }
  if (UseCMSInitiatingOccupancyOnly) {
    return false;
  }
  if (expansion_cause() == CMSExpansionCause::_satisfy_allocation) {
    if (PrintGCDetails && Verbose) {
      gclog_or_tty->print(" %s: collect because expanded for allocation ",
        short_name());
    }
    return true;
  }
  if (_cmsSpace->should_concurrent_collect()) {
    if (PrintGCDetails && Verbose) {
      gclog_or_tty->print(" %s: collect because cmsSpace says so ",
        short_name());
    }
    return true;
  }
  return false;
}

從源碼上看吏祸,這里主要分成兩類:

(1) Old Gen 空間使用占比情況與閾值比較,如果大于閾值則進(jìn)行 CMS GC

"occupancy() > initiating_occupancy()"钩蚊,occupancy 毫無疑問是 Old Gen 當(dāng)前空間的使用占比贡翘,而 initiating_occupancy 是多少呢?

_cmsGen ->init_initiating_occupancy(CMSInitiatingOccupancyFraction, CMSTriggerRatio);
...
void ConcurrentMarkSweepGeneration::init_initiating_occupancy(intx io, uintx tr) {
 assert(io <= 100 && tr <= 100, "Check the arguments");
 if (io >= 0) {
   _initiating_occupancy = (double)io / 100.0;
 } else {
   _initiating_occupancy = ((100 - MinHeapFreeRatio) +
                            (double)(tr * MinHeapFreeRatio) / 100.0)
                           / 100.0;
 }
}

可以看到當(dāng) CMSInitiatingOccupancyFraction 參數(shù)配置值大于 0砰逻,就是 “io / 100.0”鸣驱;

當(dāng) CMSInitiatingOccupancyFraction 參數(shù)配置值小于 0 時(shí)(注意,默認(rèn)是 -1)诱渤,是 “((100 - MinHeapFreeRatio) + (double)(tr * MinHeapFreeRatio) / 100.0) / 100.0”丐巫,這到底是多少呢?
是 92%勺美,這里就不貼出具體的計(jì)算過程了递胧,或許你已經(jīng)在某些書或者博客中了解過,CMSInitiatingOccupancyFraction 沒有配置赡茸,就是 92缎脾,但是其實(shí) CMSInitiatingOccupancyFraction 沒有配置是 -1,所以閾值取后者 92%占卧,并不是 CMSInitiatingOccupancyFraction 的值是 92遗菠。

(2) 接下來沒有配置 UseCMSInitiatingOccupancyOnly 的情況

這里也分成有兩小類情況:
a. Old Gen 剛因?yàn)閷?duì)象分配空間而進(jìn)行擴(kuò)容,且成功分配空間华蜒,這時(shí)會(huì)考慮進(jìn)行一次 CMS GC;
b. 根據(jù) CMS Gen 空閑鏈判斷辙纬,這里有點(diǎn)復(fù)雜,目前也沒整清楚叭喜,好在按照默認(rèn)配置其實(shí)這里返回的是 false贺拣,所以默認(rèn)是不用考慮這種觸發(fā)條件了。

  • 根據(jù)增量 GC 是否可能會(huì)失敗(悲觀策略)

什么意思呢譬涡?兩代的 GC 體系中闪幽,主要指的是 Young GC 是否會(huì)失敗。如果 Young GC 已經(jīng)失敗或者可能會(huì)失敗涡匀,JVM 就認(rèn)為需要進(jìn)行一次 CMS 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());
  }

我們看兩個(gè)判斷條件,“incremental_collection_failed()” 和 “!get_gen(0)->collection_attempt_is_safe()”
incremental_collection_failed() 這里指的是 Young GC 已經(jīng)失敗陨瘩,至于為什么會(huì)失敗一般是因?yàn)?Old Gen 沒有足夠的空間來容納晉升的對(duì)象腕够。

!get_gen(0)->collection_attempt_is_safe() 指的是新生代晉升是否安全。
通過判斷當(dāng)前 Old Gen 剩余的空間大小是否足夠容納 Young GC 晉升的對(duì)象大小拾酝。
Young GC 到底要晉升多少是無法提前知道的燕少,因此卡者,這里通過統(tǒng)計(jì)平均每次 Young GC 晉升的大小和當(dāng)前 Young GC 可能晉升的最大大小來進(jìn)行比較蒿囤。

//av_promo 是平均每次 YoungGC 晉升的大小,max_promotion_in_bytes 是當(dāng)前可能的最大晉升大谐缇觥( eden+from 當(dāng)前使用空間的大胁姆獭)
bool   res = (available >= av_promo) || (available >= max_promotion_in_bytes);
  • 根據(jù) meta space 情況判斷

這里主要看 metaspace 的 _should_concurrent_collect 標(biāo)志,這個(gè)標(biāo)志在 meta space 進(jìn)行擴(kuò)容前如果配置了 CMSClassUnloadingEnabled 參數(shù)時(shí)恒傻,會(huì)進(jìn)行設(shè)置脸侥。
這種情況下就會(huì)進(jìn)行一次 CMS GC。因此經(jīng)常會(huì)有應(yīng)用啟動(dòng)不久盈厘,Old Gen 空間占比還很小的情況下睁枕,進(jìn)行了一次 CMS GC,讓你很莫名其妙沸手,其實(shí)就是這個(gè)原因?qū)е碌摹?/p>

總結(jié)

本文梳理了 CMS GC 的 foreground collector 和 background collector 的觸發(fā)條件外遇,foreground collector 的觸發(fā)條件相對(duì)來說比較簡單,而 background collector 的觸發(fā)條件比較多契吉,分成 5 大種情況跳仿,各大種情況種還有一些小的觸發(fā)分支。尤其是在沒有配置 UseCMSInitiatingOccupancyOnly 參數(shù)的情況下捐晶,會(huì)多出很多種觸發(fā)可能菲语,一般在生產(chǎn)環(huán)境是強(qiáng)烈建議配置 UseCMSInitiatingOccupancyOnly 參數(shù),以便于能夠比較確定的執(zhí)行 CMS GC惑灵,另外山上,也方便排查 GC 原因。


個(gè)人微信公共號(hào)英支,感興趣的關(guān)注下佩憾,獲取更多技術(shù)文章

滌生-微信公共號(hào)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市潭辈,隨后出現(xiàn)的幾起案子鸯屿,更是在濱河造成了極大的恐慌澈吨,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件寄摆,死亡現(xiàn)場離奇詭異谅辣,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)婶恼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門桑阶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人勾邦,你說我怎么就攤上這事蚣录。” “怎么了眷篇?”我有些...
    開封第一講書人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵萎河,是天一觀的道長。 經(jīng)常有香客問我蕉饼,道長虐杯,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任昧港,我火速辦了婚禮擎椰,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘创肥。我一直安慰自己达舒,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開白布叹侄。 她就那樣靜靜地躺著巩搏,像睡著了一般。 火紅的嫁衣襯著肌膚如雪圈膏。 梳的紋絲不亂的頭發(fā)上塔猾,一...
    開封第一講書人閱讀 51,365評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音稽坤,去河邊找鬼丈甸。 笑死,一個(gè)胖子當(dāng)著我的面吹牛尿褪,可吹牛的內(nèi)容都是我干的睦擂。 我是一名探鬼主播,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼杖玲,長吁一口氣:“原來是場噩夢啊……” “哼顿仇!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤臼闻,失蹤者是張志新(化名)和其女友劉穎鸿吆,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體述呐,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡惩淳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了乓搬。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片思犁。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖进肯,靈堂內(nèi)的尸體忽然破棺而出激蹲,到底是詐尸還是另有隱情,我是刑警寧澤江掩,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布学辱,位于F島的核電站,受9級(jí)特大地震影響频敛,放射性物質(zhì)發(fā)生泄漏项郊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一斟赚、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧差油,春花似錦拗军、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至妆偏,卻和暖如春刃鳄,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背钱骂。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來泰國打工叔锐, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人见秽。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓愉烙,卻偏偏與公主長得像,于是被迫代替她去往敵國和親解取。 傳聞我的和親對(duì)象是個(gè)殘疾皇子步责,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354