Java中9種常見的CMS GC問題分析與解決

1. 寫在前面

| 本文主要針對 Hotspot VM 中「CMS + ParNew」組合的一些使用場景進行總結。重點經過部分源碼對根因進行分析以及對排查方法進行總結,排查過程會省略較多,另外本文專業術語較多,有必定的閱讀門檻,如未介紹清楚,還請自行查閱相關材料。html

| 總字數 2 萬左右(不包含代碼片斷),總體閱讀時間約 30min ,文章較長,能夠選擇你感興趣的場景進行研究。java

1.1 引言

自 Sun 發佈 Java 語言以來,開始使用 GC 技術來進行內存自動管理,避免了手動管理帶來的懸掛指針(Dangling Pointer)問題,很大程度上提高了開發效率,今後 GC 技術也一鳴驚人。GC 有着很是悠久的歷史,1960 年有着「Lisp 之父」和「人工智能之父」之稱的 John McCarthy 就在論文中發佈了 GC 算法,60 年以來, GC 技術的發展也日新月異,但無論是多麼前沿的收集器也都是基於三種基本算法的組合或應用,也就是說 GC 要解決的根本問題這麼多年一直都沒有變過。筆者認爲,在不太遠的未來, GC 技術依然不會過期,比起突飛猛進的新技術,GC 這門古典技術更值得咱們學習。c++

目前,互聯網上 Java 的 GC 資料要麼是主要講解理論,要麼就是針對單一場景的 GC 問題進行了剖析,對整個體系總結的資料少之又少。前車可鑑,後事之師,美團的幾位工程師蒐集了內部各類 GC 問題的分析文章,並結合我的的理解作了一些總結,但願能起到「拋磚引玉」的做用,文中如有錯誤之處,還請你們不吝指正。程序員

GC 問題處理能力能不能系統性掌握?一些影響因素都是互爲因果的問題該怎麼分析?好比一個服務 RT 忽然上漲,有 GC 耗時增大、線程 Block 增多、慢查詢增多、CPU 負載高四個表象,到底哪一個是誘因?如何判斷 GC 有沒有問題?使用 CMS 有哪些常見問題?如何判斷根因是什麼?如何解決或避免這些問題?閱讀完本文,相信你將會對 CMS GC 的問題處理有一個系統性的認知,更能遊刃有餘地解決這些問題,下面就讓咱們開始吧!面試

1.2 概覽

想要系統性地掌握 GC 問題處理,筆者這裏給出一個學習路徑,總體文章的框架也是按照這個結構展開,主要分四大步。算法

  • 創建知識體系: 從 JVM 的內存結構到垃圾收集的算法和收集器,學習 GC 的基礎知識,掌握一些經常使用的 GC 問題分析工具。
  • 肯定評價指標: 瞭解基本 GC 的評價方法,摸清如何設定獨立系統的指標,以及在業務場景中判斷 GC 是否存在問題的手段。
  • 場景調優實踐: 運用掌握的知識和系統評價指標,分析與解決九種 CMS 中常見 GC 問題場景。
  • 總結優化經驗: 對總體過程作總結並提出筆者的幾點建議,同時將總結到的經驗完善到知識體系之中。

2. GC 基礎

在正式開始前,先作些簡要鋪墊,介紹下 JVM 內存劃分、收集算法、收集器等經常使用概念介紹,基礎比較好的同窗能夠直接跳過這部分。spring

2.1 基礎概念

  • GC: GC 自己有三種語義,下文須要根據具體場景帶入不一樣的語義:shell

    • Garbage Collection:垃圾收集技術,名詞。
    • Garbage Collector:垃圾收集器,名詞。
    • Garbage Collecting:垃圾收集動做,動詞。
  • Mutator: 生產垃圾的角色,也就是咱們的應用程序,垃圾製造者,經過 Allocator 進行 allocate 和 free。
  • TLAB: Thread Local Allocation Buffer 的簡寫,基於 CAS 的獨享線程(Mutator Threads)能夠優先將對象分配在 Eden 中的一塊內存,由於是 Java 線程獨享的內存區沒有鎖競爭,因此分配速度更快,每一個 TLAB 都是一個線程獨享的。
  • Card Table: 中文翻譯爲卡表,主要是用來標記卡頁的狀態,每一個卡表項對應一個卡頁。當卡頁中一個對象引用有寫操做時,寫屏障將會標記對象所在的卡表狀態改成 dirty,卡表的本質是用來解決跨代引用的問題。具體怎麼解決的能夠參考 StackOverflow 上的這個問題 how-actually-card-table-and-writer-barrier-works,或者研讀一下 cardTableRS.app 中的源碼。

2.2 JVM 內存劃分

從 JCP(Java Community Process)的官網中能夠看到,目前 Java 版本最新已經到了 Java 16,將來的 Java 17 以及如今的 Java 11 和 Java 8 是 LTS 版本,JVM 規範也在隨着迭代在變動,因爲本文主要討論 CMS,此處仍是放 Java 8 的內存結構。數據庫

GC 主要工做在 Heap 區和 MetaSpace 區(上圖藍色部分),在 Direct Memory 中,若是使用的是 DirectByteBuffer,那麼在分配內存不夠時則是 GC 經過 Cleaner#clean 間接管理。編程

任何自動內存管理系統都會面臨的步驟:爲新對象分配空間,而後收集垃圾對象空間,下面咱們就展開介紹一下這些基礎知識。

2.3 分配對象

Java 中對象地址操做主要使用 Unsafe 調用了 C 的 allocate 和 free 兩個方法,分配方法有兩種:

  • 空閒鏈表(free list): 經過額外的存儲記錄空閒的地址,將隨機 IO 變爲順序 IO,但帶來了額外的空間消耗。
  • 碰撞指針(bump pointer): 經過一個指針做爲分界點,須要分配內存時,僅需把指針往空閒的一端移動與對象大小相等的距離,分配效率較高,但使用場景有限。

2.4 收集對象

2.4.1 識別垃圾

  • 引用計數法(Reference Counting): 對每一個對象的引用進行計數,每當有一個地方引用它時計數器 +一、引用失效則 -1,引用的計數放到對象頭中,大於 0 的對象被認爲是存活對象。雖然循環引用的問題可經過 Recycler 算法解決,可是在多線程環境下,引用計數變動也要進行昂貴的同步操做,性能較低,早期的編程語言會採用此算法。
  • 可達性分析,又稱引用鏈法(Tracing GC): 從 GC Root 開始進行對象搜索,能夠被搜索到的對象即爲可達對象,此時還不足以判斷對象是否存活/死亡,須要通過屢次標記才能更加準確地肯定,整個連通圖以外的對象即可以做爲垃圾被回收掉。目前 Java 中主流的虛擬機均採用此算法。

備註:引用計數法是能夠處理循環引用問題的,下次面試時不要再這麼說啦~ ~

2.4.2 收集算法

自從有自動內存管理出現之時就有的一些收集算法,不一樣的收集器也是在不一樣場景下進行組合。

  • Mark-Sweep(標記-清除): 回收過程主要分爲兩個階段,第一階段爲追蹤(Tracing)階段,即從 GC Root 開始遍歷對象圖,並標記(Mark)所遇到的每一個對象,第二階段爲清除(Sweep)階段,即回收器檢查堆中每個對象,並將全部未被標記的對象進行回收,整個過程不會發生對象移動。整個算法在不一樣的實現中會使用三色抽象(Tricolour Abstraction)、位圖標記(BitMap)等技術來提升算法的效率,存活對象較多時較高效。
  • Mark-Compact (標記-整理): 這個算法的主要目的就是解決在非移動式回收器中都會存在的碎片化問題,也分爲兩個階段,第一階段與 Mark-Sweep 相似,第二階段則會對存活對象按照整理順序(Compaction Order)進行整理。主要實現有雙指針(Two-Finger)回收算法、滑動回收(Lisp2)算法和引線整理(Threaded Compaction)算法等。
  • Copying(複製): 將空間分爲兩個大小相同的 From 和 To 兩個半區,同一時間只會使用其中一個,每次進行回收時將一個半區的存活對象經過複製的方式轉移到另外一個半區。有遞歸(Robert R. Fenichel 和 Jerome C. Yochelson提出)和迭代(Cheney 提出)算法,以及解決了前二者遞歸棧、緩存行等問題的近似優先搜索算法。複製算法能夠經過碰撞指針的方式進行快速地分配內存,可是也存在着空間利用率不高的缺點,另外就是存活對象比較大時複製的成本比較高。

三種算法在是否移動對象、空間和時間方面的一些對比,假設存活對象數量爲 L、堆空間大小爲 H,則:

把 mark、sweep、compaction、copying 這幾種動做的耗時放在一塊兒看,大體有這樣的關係:

雖然 compaction 與 copying 都涉及移動對象,但取決於具體算法,compaction 可能要先計算一次對象的目標地址,而後修正指針,最後再移動對象。copying 則能夠把這幾件事情合爲一體來作,因此能夠快一些。另外,還須要留意 GC 帶來的開銷不能只看 Collector 的耗時,還得看 Allocator 。若是能保證內存沒碎片,分配就能夠用 pointer bumping 方式,只須要挪一個指針就完成了分配,很是快。而若是內存有碎片就得用 freelist 之類的方式管理,分配速度一般會慢一些。

2.5 收集器

目前在 Hotspot VM 中主要有分代收集和分區收集兩大類,具體能夠看下面的這個圖,不過將來會逐漸向分區收集發展。在美團內部,有部分業務嘗試用了 ZGC(感興趣的同窗能夠學習下這篇文章 新一代垃圾回收器ZGC的探索與實踐),其他基本都停留在 CMS 和 G1 上。另外在 JDK11 後提供了一個不執行任何垃圾回收動做的回收器 Epsilon(A No-Op Garbage Collector)用做性能分析。另一個就是 Azul 的 Zing JVM,其 C4(Concurrent Continuously Compacting Collector)收集器也在業內有必定的影響力。

備註:值得一提的是,早些年國內 GC 技術的佈道者 RednaxelaFX (江湖人稱 R 大)也曾就任於 Azul,本文的一部分材料也參考了他的一些文章。

2.5.1 分代收集器

  • ParNew: 一款多線程的收集器,採用複製算法,主要工做在 Young 區,能夠經過 -XX:ParallelGCThreads 參數來控制收集的線程數,整個過程都是 STW 的,常與 CMS 組合使用。
  • CMS: 以獲取最短回收停頓時間爲目標,採用「標記-清除」算法,分 4 大步進行垃圾收集,其中初始標記和從新標記會 STW ,多數應用於互聯網站或者 B/S 系統的服務器端上,JDK9 被標記棄用,JDK14 被刪除,詳情可見 JEP 363

2.5.2 分區收集器

  • G1: 一種服務器端的垃圾收集器,應用在多處理器和大容量內存環境中,在實現高吞吐量的同時,儘量地知足垃圾收集暫停時間的要求。
  • ZGC: JDK11 中推出的一款低延遲垃圾回收器,適用於大內存低延遲服務的內存管理和回收,SPECjbb 2015 基準測試,在 128G 的大堆下,最大停頓時間才 1.68 ms,停頓時間遠勝於 G1 和 CMS。
  • Shenandoah: 由 Red Hat 的一個團隊負責開發,與 G1 相似,基於 Region 設計的垃圾收集器,但不須要 Remember Set 或者 Card Table 來記錄跨 Region 引用,停頓時間和堆的大小沒有任何關係。停頓時間與 ZGC 接近,下圖爲與 CMS 和 G1 等收集器的 benchmark。

2.5.3 經常使用收集器

目前使用最多的是 CMS 和 G1 收集器,兩者都有分代的概念,主要內存結構以下:

2.5.4 其餘收集器

以上僅列出常見收集器,除此以外還有不少,如 Metronome、Stopless、Staccato、Chicken、Clover 等實時回收器,Sapphire、Compressor、Pauseless 等併發複製/整理回收器,Doligez-Leroy-Conthier 等標記整理回收器,因爲篇幅緣由,不在此一一介紹。

2.6 經常使用工具

工欲善其事,必先利其器,此處列出一些筆者經常使用的工具,具體狀況你們能夠自由選擇,本文的問題都是使用這些工具來定位和分析的。

2.6.1 命令行終端

  • 標準終端類:jps、jinfo、jstat、jstack、jmap
  • 功能整合類:jcmd、vjtools、arthas、greys

2.6.2 可視化界面

  • 簡易:JConsole、JVisualvm、HA、GCHisto、GCViewer
  • 進階:MAT、JProfiler

命令行推薦 arthas ,可視化界面推薦 JProfiler,此外還有一些在線的平臺 gceasyheapherofastthread ,美團內部的 Scalpel(一款自研的 JVM 問題診斷工具,暫時未開源)也比較好用。

3. GC 問題判斷

在作 GC 問題排查和優化以前,咱們須要先來明確下究竟是不是 GC 直接致使的問題,或者應用代碼致使的 GC 異常,最終出現問題。

3.1 判斷 GC 有沒有問題?

3.1.1 設定評價標準

評判 GC 的兩個核心指標:

  • 延遲(Latency): 也能夠理解爲最大停頓時間,即垃圾收集過程當中一次 STW 的最長時間,越短越好,必定程度上能夠接受頻次的增大,GC 技術的主要發展方向。
  • 吞吐量(Throughput): 應用系統的生命週期內,因爲 GC 線程會佔用 Mutator 當前可用的 CPU 時鐘週期,吞吐量即爲 Mutator 有效花費的時間佔系統總運行時間的百分比,例如系統運行了 100 min,GC 耗時 1 min,則系統吞吐量爲 99%,吞吐量優先的收集器能夠接受較長的停頓。

目前各大互聯網公司的系統基本都更追求低延時,避免一次 GC 停頓的時間過長對用戶體驗形成損失,衡量指標須要結合一下應用服務的 SLA,主要以下兩點來判斷:

簡而言之,即爲一次停頓的時間不超過應用服務的 TP9999,GC 的吞吐量不小於 99.99%。舉個例子,假設某個服務 A 的 TP9999 爲 80 ms,平均 GC 停頓爲 30 ms,那麼該服務的最大停頓時間最好不要超過 80 ms,GC 頻次控制在 5 min 以上一次。若是知足不了,那就須要調優或者經過更多資源來進行並聯冗餘。(你們能夠先停下來,看看監控平臺上面的 gc.meantime 分鐘級別指標,若是超過了 6 ms 那單機 GC 吞吐量就達不到 4 個 9 了。)

備註:除了這兩個指標以外還有 Footprint(資源量大小測量)、反應速度等指標,互聯網這種實時系統追求低延遲,而不少嵌入式系統則追求 Footprint。

3.1.2 讀懂 GC Cause

拿到 GC 日誌,咱們就能夠簡單分析 GC 狀況了,經過一些工具,咱們能夠比較直觀地看到 Cause 的分佈狀況,以下圖就是使用 gceasy 繪製的圖表:

如上圖所示,咱們很清晰的就能知道是什麼緣由引發的 GC,以及每次的時間花費狀況,可是要分析 GC 的問題,先要讀懂 GC Cause,即 JVM 什麼樣的條件下選擇進行 GC 操做,具體 Cause 的分類能夠看一下 Hotspot 源碼:src/share/vm/gc/shared/gcCause.hpp 和 src/share/vm/gc/shared/gcCause.cpp 中。

const char* GCCause::to_string(GCCause::Cause cause) {
  switch (cause) {
    case _java_lang_system_gc:
      return "System.gc()";

    case _full_gc_alot:
      return "FullGCAlot";

    case _scavenge_alot:
      return "ScavengeAlot";

    case _allocation_profiler:
      return "Allocation Profiler";

    case _jvmti_force_gc:
      return "JvmtiEnv ForceGarbageCollection";

    case _gc_locker:
      return "GCLocker Initiated GC";

    case _heap_inspection:
      return "Heap Inspection Initiated GC";

    case _heap_dump:
      return "Heap Dump Initiated GC";

    case _wb_young_gc:
      return "WhiteBox Initiated Young GC";

    case _wb_conc_mark:
      return "WhiteBox Initiated Concurrent Mark";

    case _wb_full_gc:
      return "WhiteBox Initiated Full GC";

    case _no_gc:
      return "No GC";

    case _allocation_failure:
      return "Allocation Failure";

    case _tenured_generation_full:
      return "Tenured Generation Full";

    case _metadata_GC_threshold:
      return "Metadata GC Threshold";

    case _metadata_GC_clear_soft_refs:
      return "Metadata GC Clear Soft References";

    case _cms_generation_full:
      return "CMS Generation Full";

    case _cms_initial_mark:
      return "CMS Initial Mark";

    case _cms_final_remark:
      return "CMS Final Remark";

    case _cms_concurrent_mark:
      return "CMS Concurrent Mark";

    case _old_generation_expanded_on_last_scavenge:
      return "Old Generation Expanded On Last Scavenge";

    case _old_generation_too_full_to_scavenge:
      return "Old Generation Too Full To Scavenge";

    case _adaptive_size_policy:
      return "Ergonomics";

    case _g1_inc_collection_pause:
      return "G1 Evacuation Pause";

    case _g1_humongous_allocation:
      return "G1 Humongous Allocation";

    case _dcmd_gc_run:
      return "Diagnostic Command";

    case _last_gc_cause:
      return "ILLEGAL VALUE - last gc cause - ILLEGAL VALUE";

    default:
      return "unknown GCCause";
  }
  ShouldNotReachHere();
}

重點須要關注的幾個GC Cause:

  • System.gc(): 手動觸發GC操做。
  • CMS: CMS GC 在執行過程當中的一些動做,重點關注 CMS Initial Mark 和 CMS Final Remark 兩個 STW 階段。
  • Promotion Failure: Old 區沒有足夠的空間分配給 Young 區晉升的對象(即便總可用內存足夠大)。
  • Concurrent Mode Failure: CMS GC 運行期間,Old 區預留的空間不足以分配給新的對象,此時收集器會發生退化,嚴重影響 GC 性能,下面的一個案例即爲這種場景。
  • GCLocker Initiated GC: 若是線程執行在 JNI 臨界區時,恰好須要進行 GC,此時 GC Locker 將會阻止 GC 的發生,同時阻止其餘線程進入 JNI 臨界區,直到最後一個線程退出臨界區時觸發一次 GC。

什麼時機使用這些 Cause 觸發回收,你們能夠看一下 CMS 的代碼,這裏就不討論了,具體在 /src/hotspot/share/gc/cms/concurrentMarkSweepGeneration.cpp 中。

bool CMSCollector::shouldConcurrentCollect() {
  LogTarget(Trace, gc) log;

  if (_full_gc_requested) {
    log.print("CMSCollector: collect because of explicit  gc request (or GCLocker)");
    return true;
  }

  FreelistLocker x(this);
  // ------------------------------------------------------------------
  // Print out lots of information which affects the initiation of
  // a collection.
  if (log.is_enabled() && stats().valid()) {
    log.print("CMSCollector shouldConcurrentCollect: ");

    LogStream out(log);
    stats().print_on(&out);

    log.print("time_until_cms_gen_full %3.7f", stats().time_until_cms_gen_full());
    log.print("free=" SIZE_FORMAT, _cmsGen->free());
    log.print("contiguous_available=" SIZE_FORMAT, _cmsGen->contiguous_available());
    log.print("promotion_rate=%g", stats().promotion_rate());
    log.print("cms_allocation_rate=%g", stats().cms_allocation_rate());
    log.print("occupancy=%3.7f", _cmsGen->occupancy());
    log.print("initiatingOccupancy=%3.7f", _cmsGen->initiating_occupancy());
    log.print("cms_time_since_begin=%3.7f", stats().cms_time_since_begin());
    log.print("cms_time_since_end=%3.7f", stats().cms_time_since_end());
    log.print("metadata initialized %d", MetaspaceGC::should_concurrent_collect());
  }
  // ------------------------------------------------------------------

  // 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 {
   
      if (_cmsGen->occupancy() >= _bootstrap_occupancy) {
        log.print(" CMSCollector: collect for bootstrapping statistics: occupancy = %f, boot occupancy = %f",
                  _cmsGen->occupancy(), _bootstrap_occupancy);
        return true;
      }
    }
  }
  if (_cmsGen->should_concurrent_collect()) {
    log.print("CMS old gen initiated");
    return true;
  }

  CMSHeap* heap = CMSHeap::heap();
  if (heap->incremental_collection_will_fail(true /* consult_young */)) {
    log.print("CMSCollector: collect because incremental collection will fail ");
    return true;
  }

  if (MetaspaceGC::should_concurrent_collect()) {
    log.print("CMSCollector: collect for metadata allocation ");
    return true;
  }

  // CMSTriggerInterval starts a CMS cycle if enough time has passed.
  if (CMSTriggerInterval >= 0) {
    if (CMSTriggerInterval == 0) {
      // Trigger always
      return true;
    }

    // Check the CMS time since begin (we do not check the stats validity
    // as we want to be able to trigger the first CMS cycle as well)
    if (stats().cms_time_since_begin() >= (CMSTriggerInterval / ((double) MILLIUNITS))) {
      if (stats().valid()) {
        log.print("CMSCollector: collect because of trigger interval (time since last begin %3.7f secs)",
                  stats().cms_time_since_begin());
      } else {
        log.print("CMSCollector: collect because of trigger interval (first collection)");
      }
      return true;
    }
  }

  return false;
}

3.2 判斷是否是 GC 引起的問題?

究竟是結果(現象)仍是緣由,在一次 GC 問題處理的過程當中,如何判斷是 GC 致使的故障,仍是系統自己引起 GC 問題。這裏繼續拿在本文開頭提到的一個 Case:「GC 耗時增大、線程 Block 增多、慢查詢增多、CPU 負載高等四個表象,如何判斷哪一個是根因?」,筆者這裏根據本身的經驗大體整理了四種判斷方法供參考:

  • 時序分析: 先發生的事件是根因的機率更大,經過監控手段分析各個指標的異常時間點,還原事件時間線,如先觀察到 CPU 負載高(要有足夠的時間 Gap),那麼整個問題影響鏈就多是:CPU 負載高 -> 慢查詢增多 -> GC 耗時增大 -> 線程Block增多 -> RT 上漲。
  • 機率分析: 使用統計機率學,結合歷史問題的經驗進行推斷,由近到遠按類型分析,如過往慢查的問題比較多,那麼整個問題影響鏈就多是:慢查詢增多 -> GC 耗時增大 -> CPU 負載高 -> 線程 Block 增多 -> RT上漲。
  • 實驗分析: 經過故障演練等方式對問題現場進行模擬,觸發其中部分條件(一個或多個),觀察是否會發生問題,如只觸發線程 Block 就會發生問題,那麼整個問題影響鏈就多是:線程Block增多 -> CPU 負載高 -> 慢查詢增多 -> GC 耗時增大 -> RT 上漲。
  • 反證分析: 對其中某一表象進行反證分析,即判斷表象的發不發生跟結果是否有相關性,例如咱們從整個集羣的角度觀察到某些節點慢查和 CPU 都正常,但也出了問題,那麼整個問題影響鏈就多是:GC 耗時增大 -> 線程 Block 增多 -> RT 上漲。

不一樣的根因,後續的分析方法是徹底不一樣的。若是是 CPU 負載高那可能須要用火焰圖看下熱點、若是是慢查詢增多那可能須要看下 DB 狀況、若是是線程 Block 引發那可能須要看下鎖競爭的狀況,最後若是各個表象證實都沒有問題,那可能 GC 確實存在問題,能夠繼續分析 GC 問題了。

3.3 問題分類導讀

3.3.1 Mutator 類型

Mutator 的類型根據對象存活時間比例圖來看主要分爲兩種,在弱分代假說中也提到相似的說法,以下圖所示 「Survival Time」 表示對象存活時間,「Rate」 表示對象分配比例:

  • IO 交互型: 互聯網上目前大部分的服務都屬於該類型,例如分佈式 RPC、MQ、HTTP 網關服務等,對內存要求並不大,大部分對象在 TP9999 的時間內都會死亡, Young 區越大越好。
  • MEM 計算型: 主要是分佈式數據計算 Hadoop,分佈式存儲 HBase、Cassandra,自建的分佈式緩存等,對內存要求高,對象存活時間長,Old 區越大越好。

固然,除了兩者以外還有介於二者之間的場景,本篇文章主要討論第一種狀況。對象 Survival Time 分佈圖,對咱們設置 GC 參數有着很是重要的指導意義,以下圖就能夠簡單推算分代的邊界。

3.3.2 GC 問題分類

筆者選取了九種不一樣類型的 GC 問題,覆蓋了大部分場景,若是有更好的場景,歡迎在評論區給出。

  • Unexpected GC: 意外發生的 GC,實際上不須要發生,咱們能夠經過一些手段去避免。

    • Space Shock: 空間震盪問題,參見「場景一:動態擴容引發的空間震盪」。
    • Explicit GC: 顯示執行 GC 問題,參見「場景二:顯式 GC 的去與留」。
  • Partial GC: 部分收集操做的 GC,只對某些分代/分區進行回收。

    • Young GC: 分代收集裏面的 Young 區收集動做,也能夠叫作 Minor GC。

      • ParNew: Young GC 頻繁,參見「場景四:過早晉升」。
    • Old GC: 分代收集裏面的 Old 區收集動做,也能夠叫作 Major GC,有些也會叫作 Full GC,但其實這種叫法是不規範的,在 CMS 發生 Foreground GC 時纔是 Full GC,CMSScavengeBeforeRemark 參數也只是在 Remark 前觸發一次Young GC。

      • CMS: Old GC 頻繁,參見「場景五:CMS Old GC 頻繁」。
      • CMS: Old GC 不頻繁但單次耗時大,參見「場景六:單次 CMS Old GC 耗時長」。
  • MetaSpace: 元空間回收引起問題,參見「場景三:MetaSpace 區 OOM」。
  • Direct Memory: 直接內存(也能夠稱做爲堆外內存)回收引起問題,參見「場景八:堆外內存 OOM」。
  • JNI: 本地 Native 方法引起問題,參見「場景九:JNI 引起的 GC 問題」。

3.3.3 排查難度

一個問題的解決難度跟它的常見程度成反比,大部分咱們均可以經過各類搜索引擎找到相似的問題,而後用一樣的手段嘗試去解決。當一個問題在各類網站上都找不到類似的問題時,那麼可能會有兩種狀況,一種這不是一個問題,另外一種就是遇到一個隱藏比較深的問題,遇到這種問題可能就要深刻到源碼級別去調試了。如下 GC 問題場景,排查難度從上到下依次遞增。

4. 常見場景分析與解決

4.1 場景一:動態擴容引發的空間震盪

4.1.1 現象

服務剛剛啓動時 GC 次數較多,最大空間剩餘不少可是依然發生 GC,這種狀況咱們能夠經過觀察 GC 日誌或者經過監控工具來觀察堆的空間變化狀況便可。GC Cause 通常爲 Allocation Failure,且在 GC 日誌中會觀察到經歷一次 GC ,堆內各個空間的大小會被調整,以下圖所示:

4.1.2 緣由

在 JVM 的參數中 -Xms-Xmx 設置的不一致,在初始化時只會初始 -Xms 大小的空間存儲信息,每當空間不夠用時再向操做系統申請,這樣的話必然要進行一次 GC。具體是經過 ConcurrentMarkSweepGeneration::compute_new_size() 方法計算新的空間大小:

void ConcurrentMarkSweepGeneration::compute_new_size() {
  assert_locked_or_safepoint(Heap_lock);

  // If incremental collection failed, we just want to expand
  // to the limit.
  if (incremental_collection_failed()) {
    clear_incremental_collection_failed();
    grow_to_reserved();
    return;
  }

  // The heap has been compacted but not reset yet.
  // Any metric such as free() or used() will be incorrect.

  CardGeneration::compute_new_size();

  // Reset again after a possible resizing
  if (did_compact()) {
    cmsSpace()->reset_after_compaction();
  }
}

另外,若是空間剩餘不少時也會進行縮容操做,JVM 經過 -XX:MinHeapFreeRatio-XX:MaxHeapFreeRatio 來控制擴容和縮容的比例,調節這兩個值也能夠控制伸縮的時機,例如擴容即是使用 GenCollectedHeap::expand_heap_and_allocate() 來完成的,代碼以下:

HeapWord* GenCollectedHeap::expand_heap_and_allocate(size_t size, bool   is_tlab) {
  HeapWord* result = NULL;
  if (_old_gen->should_allocate(size, is_tlab)) {
    result = _old_gen->expand_and_allocate(size, is_tlab);
  }
  if (result == NULL) {
    if (_young_gen->should_allocate(size, is_tlab)) {
      result = _young_gen->expand_and_allocate(size, is_tlab);
    }
  }
  assert(result == NULL || is_in_reserved(result), "result not in heap");
  return result;
}

整個伸縮的模型理解能夠看這個圖,當 committed 的空間大小超過了低水位/高水位的大小,capacity 也會隨之調整:

4.1.3 策略

定位:觀察 CMS GC 觸發時間點 Old/MetaSpace 區的 committed 佔比是否是一個固定的值,或者像上文提到的觀察總的內存使用率也能夠。

解決:儘可能將成對出現的空間大小配置參數設置成固定的,如 -Xms-Xmx-XX:MaxNewSize-XX:NewSize-XX:MetaSpaceSize-XX:MaxMetaSpaceSize 等。

4.1.4 小結

通常來講,咱們須要保證 Java 虛擬機的堆是穩定的,確保 -Xms-Xmx 設置的是一個值(即初始值和最大值一致),得到一個穩定的堆,同理在 MetaSpace 區也有相似的問題。不過在不追求停頓時間的狀況下震盪的空間也是有利的,能夠動態地伸縮以節省空間,例如做爲富客戶端的 Java 應用。

這個問題雖然初級,可是發生的機率還真不小,尤爲是在一些規範不太健全的狀況下。

4.2 場景二:顯式 GC 的去與留

4.2.1 現象

除了擴容縮容會觸發 CMS GC 以外,還有 Old 區達到回收閾值、MetaSpace 空間不足、Young 區晉升失敗、大對象擔保失敗等幾種觸發條件,若是這些狀況都沒有發生卻觸發了 GC ?這種狀況有多是代碼中手動調用了 System.gc 方法,此時能夠找到 GC 日誌中的 GC Cause 確認下。那麼這種 GC 到底有沒有問題,翻看網上的一些資料,有人說能夠添加 -XX:+DisableExplicitGC 參數來避免這種 GC,也有人說不能加這個參數,加了就會影響 Native Memory 的回收。先說結論,筆者這裏建議保留 System.gc,那爲何要保留?咱們一塊兒來分析下。

4.2.2 緣由

找到 System.gc 在 Hotspot 中的源碼,能夠發現增長 -XX:+DisableExplicitGC 參數後,這個方法變成了一個空方法,若是沒有加的話便會調用 Universe::heap()::collect 方法,繼續跟進到這個方法中,發現 System.gc 會引起一次 STW 的 Full GC,對整個堆作收集。

JVM_ENTRY_NO_ENV(void, JVM_GC(void))
  JVMWrapper("JVM_GC");
  if (!DisableExplicitGC) {
    Universe::heap()->collect(GCCause::_java_lang_system_gc);
  }
JVM_END
void GenCollectedHeap::collect(GCCause::Cause cause) {
  if (cause == GCCause::_wb_young_gc) {
    // Young collection for the WhiteBox API.
    collect(cause, YoungGen);
  } else {
#ifdef ASSERT
  if (cause == GCCause::_scavenge_alot) {
    // Young collection only.
    collect(cause, YoungGen);
  } else {
    // Stop-the-world full collection.
    collect(cause, OldGen);
  }
#else
    // Stop-the-world full collection.
    collect(cause, OldGen);
#endif
  }
}

保留 System.gc

此處補充一個知識點,CMS GC 共分爲 Background 和 Foreground 兩種模式,前者就是咱們常規理解中的併發收集,能夠不影響正常的業務線程運行,但 Foreground Collector 卻有很大的差別,他會進行一次壓縮式 GC。此壓縮式 GC 使用的是跟 Serial Old GC 同樣的 Lisp2 算法,其使用 Mark-Compact 來作 Full GC,通常稱之爲 MSC(Mark-Sweep-Compact),它收集的範圍是 Java 堆的 Young 區和 Old 區以及 MetaSpace。由上面的算法章節中咱們知道 compact 的代價是巨大的,那麼使用 Foreground Collector 時將會帶來很是長的 STW。若是在應用程序中 System.gc 被頻繁調用,那就很是危險了。

去掉 System.gc

若是禁用掉的話就會帶來另一個內存泄漏問題,此時就須要說一下 DirectByteBuffer,它有着零拷貝等特色,被 Netty 等各類 NIO 框架使用,會使用到堆外內存。堆內存由 JVM 本身管理,堆外內存必需要手動釋放,DirectByteBuffer 沒有 Finalizer,它的 Native Memory 的清理工做是經過 sun.misc.Cleaner 自動完成的,是一種基於 PhantomReference 的清理工具,比普通的 Finalizer 輕量些。

爲 DirectByteBuffer 分配空間過程當中會顯式調用 System.gc ,但願經過 Full GC 來強迫已經無用的 DirectByteBuffer 對象釋放掉它們關聯的 Native Memory,下面爲代碼實現:

// These methods should be called whenever direct memory is allocated or
// freed.  They allow the user to control the amount of direct memory
// which a process may access.  All sizes are specified in bytes.
static void reserveMemory(long size) {

    synchronized (Bits.class) {
        if (!memoryLimitSet && VM.isBooted()) {
            maxMemory = VM.maxDirectMemory();
            memoryLimitSet = true;
        }
        if (size <= maxMemory - reservedMemory) {
            reservedMemory += size;
            return;
        }
    }

    System.gc();
    try {
        Thread.sleep(100);
    } catch (InterruptedException x) {
        // Restore interrupt status
        Thread.currentThread().interrupt();
    }
    synchronized (Bits.class) {
        if (reservedMemory + size > maxMemory)
            throw new OutOfMemoryError("Direct buffer memory");
        reservedMemory += size;
    }

}

HotSpot VM 只會在 Old GC 的時候纔會對 Old 中的對象作 Reference Processing,而在 Young GC 時只會對 Young 裏的對象作 Reference Processing。Young 中的 DirectByteBuffer 對象會在 Young GC 時被處理,也就是說,作 CMS GC 的話會對 Old 作 Reference Processing,進而能觸發 Cleaner 對已死的 DirectByteBuffer 對象作清理工做。但若是很長一段時間裏沒作過 GC 或者只作了 Young GC 的話則不會在 Old 觸發 Cleaner 的工做,那麼就可能讓原本已經死亡,但已經晉升到 Old 的 DirectByteBuffer 關聯的 Native Memory 得不到及時釋放。這幾個實現特徵使得依賴於 System.gc 觸發 GC 來保證 DirectByteMemory 的清理工做能及時完成。若是打開了 -XX:+DisableExplicitGC,清理工做就可能得不到及時完成,因而就有發生 Direct Memory 的 OOM。

4.2.3 策略

經過上面的分析看到,不管是保留仍是去掉都會有必定的風險點,不過目前互聯網中的 RPC 通訊會大量使用 NIO,因此筆者在這裏建議保留。此外 JVM 還提供了 -XX:+ExplicitGCInvokesConcurrent-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses 參數來將 System.gc 的觸發類型從 Foreground 改成 Background,同時 Background 也會作 Reference Processing,這樣的話就能大幅下降了 STW 開銷,同時也不會發生 NIO Direct Memory OOM。

4.2.4 小結

不止 CMS,在 G1 或 ZGC中開啓 ExplicitGCInvokesConcurrent 模式,都會採用高性能的併發收集方式進行收集,不過仍是建議在代碼規範方面也要作好約束,規範好 System.gc 的使用。

P.S. HotSpot 對 System.gc 有特別處理,最主要的地方體如今一次 System.gc 是否與普通 GC 同樣會觸發 GC 的統計/閾值數據的更新,HotSpot 裏的許多 GC 算法都帶有自適應的功能,會根據先前收集的效率來決定接下來的 GC 中使用的參數,但 System.gc 默認不更新這些統計數據,避免用戶強行 GC 對這些自適應功能的干擾(能夠參考 -XX:+UseAdaptiveSizePolicyWithSystemGC 參數,默認是 false)。

4.3 場景三:MetaSpace 區 OOM

4.3.1 現象

JVM 在啓動後或者某個時間點開始,MetaSpace 的已使用大小在持續增加,同時每次 GC 也沒法釋放,調大 MetaSpace 空間也沒法完全解決

4.3.2 緣由

在討論爲何會 OOM 以前,咱們先來看一下這個區裏面會存什麼數據,Java7 以前字符串常量池被放到了 Perm 區,全部被 intern 的 String 都會被存在這裏,因爲 String.intern 是不受控的,因此 -XX:MaxPermSize 的值也不太好設置,常常會出現 java.lang.OutOfMemoryError: PermGen space 異常,因此在 Java7 以後常量池等字面量(Literal)、類靜態變量(Class Static)、符號引用(Symbols Reference)等幾項被移到 Heap 中。而 Java8 以後 PermGen 也被移除,取而代之的是 MetaSpace。

在最底層,JVM 經過 mmap 接口向操做系統申請內存映射,每次申請 2MB 空間,這裏是虛擬內存映射,不是真的就消耗了主存的 2MB,只有以後在使用的時候纔會真的消耗內存。申請的這些內存放到一個鏈表中 VirtualSpaceList,做爲其中的一個 Node。

在上層,MetaSpace 主要由 Klass Metaspace 和 NoKlass Metaspace 兩大部分組成。

  • Klass MetaSpace: 就是用來存 Klass 的,就是 Class 文件在 JVM 裏的運行時數據結構,這部分默認放在 Compressed Class Pointer Space 中,是一塊連續的內存區域,緊接着 Heap。Compressed Class Pointer Space 不是必須有的,若是設置了 -XX:-UseCompressedClassPointers,或者 -Xmx 設置大於 32 G,就不會有這塊內存,這種狀況下 Klass 都會存在 NoKlass Metaspace 裏。
  • NoKlass MetaSpace: 專門來存 Klass 相關的其餘的內容,好比 Method,ConstantPool 等,能夠由多塊不連續的內存組成。雖然叫作 NoKlass Metaspace,可是也其實能夠存 Klass 的內容,上面已經提到了對應場景。

具體的定義均可以在源碼 shared/vm/memory/metaspace.hpp 中找到:

class Metaspace : public AllStatic {

  friend class MetaspaceShared;

 public:
  enum MetadataType {
    ClassType,
    NonClassType,
    MetadataTypeCount
  };
  enum MetaspaceType {
    ZeroMetaspaceType = 0,
    StandardMetaspaceType = ZeroMetaspaceType,
    BootMetaspaceType = StandardMetaspaceType + 1,
    AnonymousMetaspaceType = BootMetaspaceType + 1,
    ReflectionMetaspaceType = AnonymousMetaspaceType + 1,
    MetaspaceTypeCount
  };

 private:

  // Align up the word size to the allocation word size
  static size_t align_word_size_up(size_t);

  // Aligned size of the metaspace.
  static size_t _compressed_class_space_size;

  static size_t compressed_class_space_size() {
    return _compressed_class_space_size;
  }

  static void set_compressed_class_space_size(size_t size) {
    _compressed_class_space_size = size;
  }

  static size_t _first_chunk_word_size;
  static size_t _first_class_chunk_word_size;

  static size_t _commit_alignment;
  static size_t _reserve_alignment;
  DEBUG_ONLY(static bool   _frozen;)

  // Virtual Space lists for both classes and other metadata
  static metaspace::VirtualSpaceList* _space_list;
  static metaspace::VirtualSpaceList* _class_space_list;

  static metaspace::ChunkManager* _chunk_manager_metadata;
  static metaspace::ChunkManager* _chunk_manager_class;

  static const MetaspaceTracer* _tracer;
}

MetaSpace 的對象爲何沒法釋放,咱們看下面兩點:

  • MetaSpace 內存管理: 類和其元數據的生命週期與其對應的類加載器相同,只要類的類加載器是存活的,在 Metaspace 中的類元數據也是存活的,不能被回收。每一個加載器有單獨的存儲空間,經過 ClassLoaderMetaspace 來進行管理 SpaceManager* 的指針,相互隔離的。
  • MetaSpace 彈性伸縮: 因爲 MetaSpace 空間和 Heap 並不在一塊兒,因此這塊的空間能夠不用設置或者單獨設置,通常狀況下避免 MetaSpace 耗盡 VM 內存都會設置一個 MaxMetaSpaceSize,在運行過程當中,若是實際大小小於這個值,JVM 就會經過 -XX:MinMetaspaceFreeRatio-XX:MaxMetaspaceFreeRatio 兩個參數動態控制整個 MetaSpace 的大小,具體使用能夠看 MetaSpaceGC::compute_new_size() 方法(下方代碼),這個方法會在 CMSCollector 和 G1CollectorHeap 等幾個收集器執行 GC 時調用。這個裏面會根據 used_after_gcMinMetaspaceFreeRatioMaxMetaspaceFreeRatio 這三個值計算出來一個新的 _capacity_until_GC 值(水位線)。而後根據實際的 _capacity_until_GC 值使用 MetaspaceGC::inc_capacity_until_GC()MetaspaceGC::dec_capacity_until_GC() 進行 expand 或 shrink,這個過程也能夠參照場景一中的伸縮模型進行理解。
void MetaspaceGC::compute_new_size() {
  assert(_shrink_factor <= 100, "invalid shrink factor");
  uint current_shrink_factor = _shrink_factor;
  _shrink_factor = 0;
  const size_t used_after_gc = MetaspaceUtils::committed_bytes();
  const size_t capacity_until_GC = MetaspaceGC::capacity_until_GC();

  const double minimum_free_percentage = MinMetaspaceFreeRatio / 100.0;
  const double maximum_used_percentage = 1.0 - minimum_free_percentage;

  const double min_tmp = used_after_gc / maximum_used_percentage;
  size_t minimum_desired_capacity =
    (size_t)MIN2(min_tmp, double(max_uintx));
  // Don't shrink less than the initial generation size
  minimum_desired_capacity = MAX2(minimum_desired_capacity,
                                  MetaspaceSize);

  log_trace(gc, metaspace)("MetaspaceGC::compute_new_size: ");
  log_trace(gc, metaspace)("    minimum_free_percentage: %6.2f  maximum_used_percentage: %6.2f",
                           minimum_free_percentage, maximum_used_percentage);
  log_trace(gc, metaspace)("     used_after_gc       : %6.1fKB", used_after_gc / (double) K);


  size_t shrink_bytes = 0;
  if (capacity_until_GC < minimum_desired_capacity) {
    // If we have less capacity below the metaspace HWM, then
    // increment the HWM.
    size_t expand_bytes = minimum_desired_capacity - capacity_until_GC;
    expand_bytes = align_up(expand_bytes, Metaspace::commit_alignment());
    // Don't expand unless it's significant
    if (expand_bytes >= MinMetaspaceExpansion) {
      size_t new_capacity_until_GC = 0;
      bool succeeded = MetaspaceGC::inc_capacity_until_GC(expand_bytes, &new_capacity_until_GC);
      assert(succeeded, "Should always succesfully increment HWM when at safepoint");

      Metaspace::tracer()->report_gc_threshold(capacity_until_GC,
                                               new_capacity_until_GC,
                                               MetaspaceGCThresholdUpdater::ComputeNewSize);
      log_trace(gc, metaspace)("    expanding:  minimum_desired_capacity: %6.1fKB  expand_bytes: %6.1fKB  MinMetaspaceExpansion: %6.1fKB  new metaspace HWM:  %6.1fKB",
                               minimum_desired_capacity / (double) K,
                               expand_bytes / (double) K,
                               MinMetaspaceExpansion / (double) K,
                               new_capacity_until_GC / (double) K);
    }
    return;
  }

  // No expansion, now see if we want to shrink
  // We would never want to shrink more than this
  assert(capacity_until_GC >= minimum_desired_capacity,
         SIZE_FORMAT " >= " SIZE_FORMAT,
         capacity_until_GC, minimum_desired_capacity);
  size_t max_shrink_bytes = capacity_until_GC - minimum_desired_capacity;

  // Should shrinking be considered?
  if (MaxMetaspaceFreeRatio < 100) {
    const double maximum_free_percentage = MaxMetaspaceFreeRatio / 100.0;
    const double minimum_used_percentage = 1.0 - maximum_free_percentage;
    const double max_tmp = used_after_gc / minimum_used_percentage;
    size_t maximum_desired_capacity = (size_t)MIN2(max_tmp, double(max_uintx));
    maximum_desired_capacity = MAX2(maximum_desired_capacity,
                                    MetaspaceSize);
    log_trace(gc, metaspace)("    maximum_free_percentage: %6.2f  minimum_used_percentage: %6.2f",
                             maximum_free_percentage, minimum_used_percentage);
    log_trace(gc, metaspace)("    minimum_desired_capacity: %6.1fKB  maximum_desired_capacity: %6.1fKB",
                             minimum_desired_capacity / (double) K, maximum_desired_capacity / (double) K);

    assert(minimum_desired_capacity <= maximum_desired_capacity,
           "sanity check");

    if (capacity_until_GC > maximum_desired_capacity) {
      // Capacity too large, compute shrinking size
      shrink_bytes = capacity_until_GC - maximum_desired_capacity;
      shrink_bytes = shrink_bytes / 100 * current_shrink_factor;

      shrink_bytes = align_down(shrink_bytes, Metaspace::commit_alignment());

      assert(shrink_bytes <= max_shrink_bytes,
             "invalid shrink size " SIZE_FORMAT " not <= " SIZE_FORMAT,
             shrink_bytes, max_shrink_bytes);
      if (current_shrink_factor == 0) {
        _shrink_factor = 10;
      } else {
        _shrink_factor = MIN2(current_shrink_factor * 4, (uint) 100);
      }
      log_trace(gc, metaspace)("    shrinking:  initThreshold: %.1fK  maximum_desired_capacity: %.1fK",
                               MetaspaceSize / (double) K, maximum_desired_capacity / (double) K);
      log_trace(gc, metaspace)("    shrink_bytes: %.1fK  current_shrink_factor: %d  new shrink factor: %d  MinMetaspaceExpansion: %.1fK",
                               shrink_bytes / (double) K, current_shrink_factor, _shrink_factor, MinMetaspaceExpansion / (double) K);
    }
  }

  // Don't shrink unless it's significant
  if (shrink_bytes >= MinMetaspaceExpansion &&
      ((capacity_until_GC - shrink_bytes) >= MetaspaceSize)) {
    size_t new_capacity_until_GC = MetaspaceGC::dec_capacity_until_GC(shrink_bytes);
    Metaspace::tracer()->report_gc_threshold(capacity_until_GC,
                                             new_capacity_until_GC,
                                             MetaspaceGCThresholdUpdater::ComputeNewSize);
  }
}

由場景一可知,爲了不彈性伸縮帶來的額外 GC 消耗,咱們會將 -XX:MetaSpaceSize-XX:MaxMetaSpaceSize 兩個值設置爲固定的,可是這樣也會致使在空間不夠的時候沒法擴容,而後頻繁地觸發 GC,最終 OOM。因此關鍵緣由就是 ClassLoader 不停地在內存中 load 了新的 Class ,通常這種問題都發生在動態類加載等狀況上。

4.3.3 策略

瞭解大概什麼緣由後,如何定位和解決就很簡單了,能夠 dump 快照以後經過 JProfiler 或 MAT 觀察 Classes 的 Histogram(直方圖) 便可,或者直接經過命令便可定位, jcmd 打幾回 Histogram 的圖,看一下具體是哪一個包下的 Class 增長較多就能夠定位了。不過有時候也要結合InstBytes、KlassBytes、Bytecodes、MethodAll 等幾項指標綜合來看下。以下圖即是筆者使用 jcmd 排查到一個 Orika 的問題。

jcmd <PID> GC.class_stats|awk '{print$13}'|sed  's/\(.*\)\.\(.*\)/\1/g'|sort |uniq -c|sort -nrk1

若是沒法從總體的角度定位,能夠添加 -XX:+TraceClassLoading-XX:+TraceClassUnLoading 參數觀察詳細的類加載和卸載信息。

4.3.4 小結

原理理解比較複雜,但定位和解決問題會比較簡單,常常會出問題的幾個點有 Orika 的 classMap、JSON 的 ASMSerializer、Groovy 動態加載類等,基本都集中在反射、Javasisit 字節碼加強、CGLIB 動態代理、OSGi 自定義類加載器等的技術點上。另外就是及時給 MetaSpace 區的使用率加一個監控,若是指標有波動提早發現並解決問題。

4.4 場景四:過早晉升 *

4.4.1 現象

這種場景主要發生在分代的收集器上面,專業的術語稱爲「Premature Promotion」。90% 的對象朝生夕死,只有在 Young 區經歷過幾回 GC 的洗禮後纔會晉升到 Old 區,每經歷一次 GC 對象的 GC Age 就會增加 1,最大經過 -XX:MaxTenuringThreshold 來控制。

過早晉升通常不會直接影響 GC,總會伴隨着浮動垃圾、大對象擔保失敗等問題,但這些問題不是馬上發生的,咱們能夠觀察如下幾種現象來判斷是否發生了過早晉升。

分配速率接近於晉升速率,對象晉升年齡較小。

GC 日誌中出現「Desired survivor size 107347968 bytes, new threshold 1(max 6)」等信息,說明此時經歷過一次 GC 就會放到 Old 區。

Full GC 比較頻繁,且經歷過一次 GC 以後 Old 區的變化比例很是大

好比說 Old 區觸發的回收閾值是 80%,經歷過一次 GC 以後降低到了 10%,這就說明 Old 區的 70% 的對象存活時間其實很短,以下圖所示,Old 區大小每次 GC 後從 2.1G 回收到 300M,也就是說回收掉了 1.8G 的垃圾,只有 300M 的活躍對象。整個 Heap 目前是 4G,活躍對象只佔了不到十分之一。

過早晉升的危害:

  • Young GC 頻繁,總的吞吐量降低。
  • Full GC 頻繁,可能會有較大停頓。

4.4.2 緣由

主要的緣由有如下兩點:

  • Young/Eden 區太小: 太小的直接後果就是 Eden 被裝滿的時間變短,本應該回收的對象參與了 GC 並晉升,Young GC 採用的是複製算法,由基礎篇咱們知道 copying 耗時遠大於 mark,也就是 Young GC 耗時本質上就是 copy 的時間(CMS 掃描 Card Table 或 G1 掃描 Remember Set 出問題的狀況另說),沒來及回收的對象增大了回收的代價,因此 Young GC 時間增長,同時又沒法快速釋放空間,Young GC 次數也跟着增長。
  • 分配速率過大: 能夠觀察出問題先後 Mutator 的分配速率,若是有明顯波動能夠嘗試觀察網卡流量、存儲類中間件慢查詢日誌等信息,看是否有大量數據被加載到內存中。

同時沒法 GC 掉對象還會帶來另一個問題,引起動態年齡計算:JVM 經過 -XX:MaxTenuringThreshold 參數來控制晉升年齡,每通過一次 GC,年齡就會加一,達到最大年齡就能夠進入 Old 區,最大值爲 15(由於 JVM 中使用 4 個比特來表示對象的年齡)。設定固定的 MaxTenuringThreshold 值做爲晉升條件:

  • MaxTenuringThreshold 若是設置得過大,本來應該晉升的對象一直停留在 Survivor 區,直到 Survivor 區溢出,一旦溢出發生,Eden + Survivor 中對象將再也不依據年齡所有提高到 Old 區,這樣對象老化的機制就失效了。
  • MaxTenuringThreshold 若是設置得太小,過早晉升即對象不能在 Young 區充分被回收,大量短時間對象被晉升到 Old 區,Old 區空間迅速增加,引發頻繁的 Major GC,分代回收失去了意義,嚴重影響 GC 性能。

相同應用在不一樣時間的表現不一樣,特殊任務的執行或者流量成分的變化,都會致使對象的生命週期分佈發生波動,那麼固定的閾值設定,由於沒法動態適應變化,會形成和上面問題,因此 Hotspot 會使用動態計算的方式來調整晉升的閾值。

具體動態計算能夠看一下 Hotspot 源碼,具體在 /src/hotspot/share/gc/shared/ageTable.cpp 的 compute_tenuring_threshold 方法中:

uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
  //TargetSurvivorRatio默認50,意思是:在回收以後但願survivor區的佔用率達到這個比例
  size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100);
  size_t total = 0;
  uint age = 1;
  assert(sizes[0] == 0, "no objects with age zero should be recorded");
  while (age < table_size) {//table_size=16
    total += sizes[age];
    //若是加上這個年齡的全部對象的大小以後,佔用量>指望的大小,就設置age爲新的晉升閾值
    if (total > desired_survivor_size) break;
    age++;
  }

  uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
  if (PrintTenuringDistribution || UsePerfData) {

    //打印指望的survivor的大小以及新計算出來的閾值,和設置的最大閾值
    if (PrintTenuringDistribution) {
      gclog_or_tty->cr();
      gclog_or_tty->print_cr("Desired survivor size " SIZE_FORMAT " bytes, new threshold %u (max %u)",
        desired_survivor_size*oopSize, result, (int) MaxTenuringThreshold);
    }

    total = 0;
    age = 1;
    while (age < table_size) {
      total += sizes[age];
      if (sizes[age] > 0) {
        if (PrintTenuringDistribution) {
          gclog_or_tty->print_cr("- age %3u: " SIZE_FORMAT_W(10) " bytes, " SIZE_FORMAT_W(10) " total",
                                        age,    sizes[age]*oopSize,          total*oopSize);
        }
      }
      if (UsePerfData) {
        _perf_sizes[age]->set_value(sizes[age]*oopSize);
      }
      age++;
    }
    if (UsePerfData) {
      SharedHeap* sh = SharedHeap::heap();
      CollectorPolicy* policy = sh->collector_policy();
      GCPolicyCounters* gc_counters = policy->counters();
      gc_counters->tenuring_threshold()->set_value(result);
      gc_counters->desired_survivor_size()->set_value(
        desired_survivor_size*oopSize);
    }
  }

  return result;
}

能夠看到 Hotspot 遍歷全部對象時,從全部年齡爲 0 的對象佔用的空間開始累加,若是加上年齡等於 n 的全部對象的空間以後,使用 Survivor 區的條件值(TargetSurvivorRatio / 100,TargetSurvivorRatio 默認值爲 50)進行判斷,若大於這個值則結束循環,將 n 和 MaxTenuringThreshold 比較,若 n 小,則閾值爲 n,若 n 大,則只能去設置最大閾值爲 MaxTenuringThreshold。動態年齡觸發後致使更多的對象進入了 Old 區,形成資源浪費

4.4.3 策略

知道問題緣由後咱們就有解決的方向,若是是 Young/Eden 區太小,咱們能夠在總的 Heap 內存不變的狀況下適當增大 Young 區,具體怎麼增長?通常狀況下 Old 的大小應當爲活躍對象的 2~3 倍左右,考慮到浮動垃圾問題最好在 3 倍左右,剩下的均可以分給 Young 區。

拿筆者的一次典型過早晉升優化來看,原配置爲 Young 1.2G + Old 2.8G,經過觀察 CMS GC 的狀況找到存活對象大概爲 300~400M,因而調整 Old 1.5G 左右,剩下 2.5G 分給 Young 區。僅僅調了一個 Young 區大小參數(-Xmn),整個 JVM 一分鐘 Young GC 從 26 次下降到了 11 次,單次時間也沒有增長,總的 GC 時間從 1100ms 下降到了 500ms,CMS GC 次數也從 40 分鐘左右一次下降到了 7 小時 30 分鐘一次。


若是是分配速率過大:

  • 偶發較大:經過內存分析工具找到問題代碼,從業務邏輯上作一些優化。
  • 一直較大:當前的 Collector 已經不知足 Mutator 的指望了,這種狀況要麼擴容 Mutator 的 VM,要麼調整 GC 收集器類型或加大空間。

4.4.4 小結

過早晉升問題通常不會特別明顯,但日積月累以後可能會爆發一波收集器退化之類的問題,因此咱們仍是要提早避免掉的,能夠看看本身系統裏面是否有這些現象,若是比較匹配的話,能夠嘗試優化一下。一行代碼優化的 ROI 仍是很高的。

若是在觀察 Old 區先後比例變化的過程當中,發現能夠回收的比例很是小,如從 80% 只回收到了 60%,說明咱們大部分對象都是存活的,Old 區的空間能夠適當調大些。

4.4.5 加餐

關於在調整 Young 與 Old 的比例時,如何選取具體的 NewRatio 值,這裏將問題抽象成爲一個蓄水池模型,找到如下關鍵衡量指標,你們能夠根據本身場景進行推算。

  • NewRatio 的值 r 與 va、vp、vyc、voc、rs 等值存在必定函數相關性(rs 越小 r 越大、r 越小 vp 越小,...,以前嘗試使用 NN 來輔助建模,但目前尚未徹底算出具體的公式,有想法的同窗能夠在評論區給出你的答案)。
  • 總停頓時間 T 爲 Young GC 總時間 Tyc 和 Old GC 總時間 Toc 之和,其中 Tyc 與 vyc 和 vp 相關,Toc 與 voc相關。
  • 忽略掉 GC 時間後,兩次 Young GC 的時間間隔要大於 TP9999 時間,這樣儘可能讓對象在 Eden 區就被回收,能夠減小不少停頓。

4.5 場景五:CMS Old GC 頻繁*

4.5.1 現象

Old 區頻繁的作 CMS GC,可是每次耗時不是特別長,總體最大 STW 也在可接受範圍內,但因爲 GC 太頻繁致使吞吐降低比較多。

4.5.2 緣由

這種狀況比較常見,基本都是一次 Young GC 完成後,負責處理 CMS GC 的一個後臺線程 concurrentMarkSweepThread 會不斷地輪詢,使用 shouldConcurrentCollect() 方法作一次檢測,判斷是否達到了回收條件。若是達到條件,使用 collect_in_background() 啓動一次 Background 模式 GC。輪詢的判斷是使用 sleepBeforeNextCycle() 方法,間隔週期爲 -XX:CMSWaitDuration 決定,默認爲2s。

具體代碼在: src/hotspot/share/gc/cms/concurrentMarkSweepThread.cpp。

void ConcurrentMarkSweepThread::run_service() {
  assert(this == cmst(), "just checking");

  if (BindCMSThreadToCPU && !os::bind_to_processor(CPUForCMSThread)) {
    log_warning(gc)("Couldn't bind CMS thread to processor " UINTX_FORMAT, CPUForCMSThread);
  }

  while (!should_terminate()) {
    sleepBeforeNextCycle();
    if (should_terminate()) break;
    GCIdMark gc_id_mark;
    GCCause::Cause cause = _collector->_full_gc_requested ?
      _collector->_full_gc_cause : GCCause::_cms_concurrent_mark;
    _collector->collect_in_background(cause);
  }
  verify_ok_to_terminate();
}

void ConcurrentMarkSweepThread::sleepBeforeNextCycle() {
  while (!should_terminate()) {
    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
  }
}

判斷是否進行回收的代碼在:/src/hotspot/share/gc/cms/concurrentMarkSweepGeneration.cpp。

bool CMSCollector::shouldConcurrentCollect() {
  LogTarget(Trace, gc) log;

  if (_full_gc_requested) {
    log.print("CMSCollector: collect because of explicit  gc request (or GCLocker)");
    return true;
  }

  FreelistLocker x(this);
  // ------------------------------------------------------------------
  // Print out lots of information which affects the initiation of
  // a collection.
  if (log.is_enabled() && stats().valid()) {
    log.print("CMSCollector shouldConcurrentCollect: ");

    LogStream out(log);
    stats().print_on(&out);

    log.print("time_until_cms_gen_full %3.7f", stats().time_until_cms_gen_full());
    log.print("free=" SIZE_FORMAT, _cmsGen->free());
    log.print("contiguous_available=" SIZE_FORMAT, _cmsGen->contiguous_available());
    log.print("promotion_rate=%g", stats().promotion_rate());
    log.print("cms_allocation_rate=%g", stats().cms_allocation_rate());
    log.print("occupancy=%3.7f", _cmsGen->occupancy());
    log.print("initiatingOccupancy=%3.7f", _cmsGen->initiating_occupancy());
    log.print("cms_time_since_begin=%3.7f", stats().cms_time_since_begin());
    log.print("cms_time_since_end=%3.7f", stats().cms_time_since_end());
    log.print("metadata initialized %d", MetaspaceGC::should_concurrent_collect());
  }
  // ------------------------------------------------------------------
  if (!UseCMSInitiatingOccupancyOnly) {
    if (stats().valid()) {
      if (stats().time_until_cms_start() == 0.0) {
        return true;
      }
    } else {
  
      if (_cmsGen->occupancy() >= _bootstrap_occupancy) {
        log.print(" CMSCollector: collect for bootstrapping statistics: occupancy = %f, boot occupancy = %f",
                  _cmsGen->occupancy(), _bootstrap_occupancy);
        return true;
      }
    }
  }

  if (_cmsGen->should_concurrent_collect()) {
    log.print("CMS old gen initiated");
    return true;
  }

  // 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.
  CMSHeap* heap = CMSHeap::heap();
  if (heap->incremental_collection_will_fail(true /* consult_young */)) {
    log.print("CMSCollector: collect because incremental collection will fail ");
    return true;
  }

  if (MetaspaceGC::should_concurrent_collect()) {
    log.print("CMSCollector: collect for metadata allocation ");
    return true;
  }

  // CMSTriggerInterval starts a CMS cycle if enough time has passed.
  if (CMSTriggerInterval >= 0) {
    if (CMSTriggerInterval == 0) {
      // Trigger always
      return true;
    }

    // Check the CMS time since begin (we do not check the stats validity
    // as we want to be able to trigger the first CMS cycle as well)
    if (stats().cms_time_since_begin() >= (CMSTriggerInterval / ((double) MILLIUNITS))) {
      if (stats().valid()) {
        log.print("CMSCollector: collect because of trigger interval (time since last begin %3.7f secs)",
                  stats().cms_time_since_begin());
      } else {
        log.print("CMSCollector: collect because of trigger interval (first collection)");
      }
      return true;
    }
  }

  return false;
}

分析其中邏輯判斷是否觸發 GC,分爲如下幾種狀況:

  • 觸發 CMS GC: 經過調用 _collector->collect_in_background() 進行觸發 Background GC 。

    • CMS 默認採用 JVM 運行時的統計數據判斷是否須要觸發 CMS GC,若是須要根據 -XX:CMSInitiatingOccupancyFraction 的值進行判斷,須要設置參數 -XX:+UseCMSInitiatingOccupancyOnly
    • 若是開啓了 -XX:UseCMSInitiatingOccupancyOnly 參數,判斷當前 Old 區使用率是否大於閾值,則觸發 CMS GC,該閾值能夠經過參數 -XX:CMSInitiatingOccupancyFraction 進行設置,若是沒有設置,默認爲 92%。
    • 若是以前的 Young GC 失敗過,或者下次 Young 區執行 Young GC 可能失敗,這兩種狀況下都須要觸發 CMS GC。
    • CMS 默認不會對 MetaSpace 或 Perm 進行垃圾收集,若是但願對這些區域進行垃圾收集,須要設置參數 -XX:+CMSClassUnloadingEnabled
  • 觸發 Full GC: 直接進行 Full GC,這種狀況到場景七中展開說明。

    • 若是 _full_gc_requested 爲真,說明有明確的需求要進行 GC,好比調用 System.gc。
    • 在 Eden 區爲對象或 TLAB 分配內存失敗,致使一次 Young GC,在 GenCollectorPolicy 類的 satisfy_failed_allocation() 方法中進行判斷。

你們能夠看一下源碼中的日誌打印,經過日誌咱們就能夠比較清楚地知道具體的緣由,而後就能夠着手分析了。

4.5.3 策略

咱們這裏仍是拿最多見的達到回收比例這個場景來講,與過早晉升不一樣的是這些對象確實存活了一段時間,Survival Time 超過了 TP9999 時間,可是又達不到長期存活,如各類數據庫、網絡連接,帶有失效時間的緩存等。

處理這種常規內存泄漏問題基本是一個思路,主要步驟以下:

Dump Diff 和 Leak Suspects 比較直觀就不介紹了,這裏說下其它幾個關鍵點:

  • 內存 Dump: 使用 jmap、arthas 等 dump 堆進行快照時記得摘掉流量,同時分別在 CMS GC 的發生先後分別 dump 一次
  • 分析 Top Component: 要記得按照對象、類、類加載器、包等多個維度觀察 Histogram,同時使用 outgoing 和 incoming 分析關聯的對象,另外就是 Soft Reference 和 Weak Reference、Finalizer 等也要看一下。
  • 分析 Unreachable: 重點看一下這個,關注下 Shallow 和 Retained 的大小。以下圖所示,筆者以前一次 GC 優化,就根據 Unreachable Objects 發現了 Hystrix 的滑動窗口問題。

4.5.4 小結

通過整個流程下來基本就能定位問題了,不過在優化的過程當中記得使用控制變量的方法來優化,防止一些會加重問題的改動被掩蓋。

4.6 場景六:單次 CMS Old GC 耗時長*

4.6.1 現象

CMS GC 單次 STW 最大超過 1000ms,不會頻繁發生,以下圖所示最長達到了 8000ms。某些場景下會引發「雪崩效應」,這種場景很是危險,咱們應該儘可能避免出現。

4.6.2 緣由

CMS 在回收的過程當中,STW 的階段主要是 Init Mark 和 Final Remark 這兩個階段,也是致使 CMS Old GC 最多的緣由,另外有些狀況就是在 STW 前等待 Mutator 的線程到達 SafePoint 也會致使時間過長,但這種狀況較少,咱們在此處主要討論前者。發生收集器退化或者碎片壓縮的場景請看場景七。

想要知道這兩個階段爲何會耗時,咱們須要先看一下這兩個階段都會幹什麼。

核心代碼都在 /src/hotspot/share/gc/cms/concurrentMarkSweepGeneration.cpp 中,內部有個線程 ConcurrentMarkSweepThread 輪詢來校驗,Old 區的垃圾回收相關細節被徹底封裝在 CMSCollector 中,調用入口就是 ConcurrentMarkSweepThread 調用的 CMSCollector::collect_in_backgroundConcurrentMarkSweepGeneration 調用的 CMSCollector::collect 方法,此處咱們討論大多數場景的 collect_in_background。整個過程當中會 STW 的主要是 initial Mark 和 Final Remark,核心代碼在 VM_CMS_Initial_Mark / VM_CMS_Final_Remark 中,執行時須要將執行權交由 VMThread 來執行。

  • CMS Init Mark執行步驟,實如今 CMSCollector::checkpointRootsInitialWork()CMSParInitialMarkTask::work 中,總體步驟和代碼以下:
void CMSCollector::checkpointRootsInitialWork() {
  assert(SafepointSynchronize::is_at_safepoint(), "world should be stopped");
  assert(_collectorState == InitialMarking, "just checking");

  // Already have locks.
  assert_lock_strong(bitMapLock());
  assert(_markBitMap.isAllClear(), "was reset at end of previous cycle");

  // Setup the verification and class unloading state for this
  // CMS collection cycle.
  setup_cms_unloading_and_verification_state();

  GCTraceTime(Trace, gc, phases) ts("checkpointRootsInitialWork", _gc_timer_cm);

  // Reset all the PLAB chunk arrays if necessary.
  if (_survivor_plab_array != NULL && !CMSPLABRecordAlways) {
    reset_survivor_plab_arrays();
  }

  ResourceMark rm;
  HandleMark  hm;

  MarkRefsIntoClosure notOlder(_span, &_markBitMap);
  CMSHeap* heap = CMSHeap::heap();

  verify_work_stacks_empty();
  verify_overflow_empty();

  heap->ensure_parsability(false);  // fill TLABs, but no need to retire them
  // Update the saved marks which may affect the root scans.
  heap->save_marks();

  // weak reference processing has not started yet.
  ref_processor()->set_enqueuing_is_done(false);

  // Need to remember all newly created CLDs,
  // so that we can guarantee that the remark finds them.
  ClassLoaderDataGraph::remember_new_clds(true);

  // Whenever a CLD is found, it will be claimed before proceeding to mark
  // the klasses. The claimed marks need to be cleared before marking starts.
  ClassLoaderDataGraph::clear_claimed_marks();

  print_eden_and_survivor_chunk_arrays();

  {
    if (CMSParallelInitialMarkEnabled) {
      // The parallel version.
      WorkGang* workers = heap->workers();
      assert(workers != NULL, "Need parallel worker threads.");
      uint n_workers = workers->active_workers();

      StrongRootsScope srs(n_workers);

      CMSParInitialMarkTask tsk(this, &srs, n_workers);
      initialize_sequential_subtasks_for_young_gen_rescan(n_workers);
      // If the total workers is greater than 1, then multiple workers
      // may be used at some time and the initialization has been set
      // such that the single threaded path cannot be used.
      if (workers->total_workers() > 1) {
        workers->run_task(&tsk);
      } else {
        tsk.work(0);
      }
    } else {
      // The serial version.
      CLDToOopClosure cld_closure(&notOlder, true);
      heap->rem_set()->prepare_for_younger_refs_iterate(false); // Not parallel.

      StrongRootsScope srs(1);

      heap->cms_process_roots(&srs,
                             true,   // young gen as roots
                             GenCollectedHeap::ScanningOption(roots_scanning_options()),
                             should_unload_classes(),
                             &notOlder,
                             &cld_closure);
    }
  }

  // Clear mod-union table; it will be dirtied in the prologue of
  // CMS generation per each young generation collection.
  assert(_modUnionTable.isAllClear(),
       "Was cleared in most recent final checkpoint phase"
       " or no bits are set in the gc_prologue before the start of the next "
       "subsequent marking phase.");

  assert(_ct->cld_rem_set()->mod_union_is_clear(), "Must be");
  // Save the end of the used_region of the constituent generations
  // to be used to limit the extent of sweep in each generation.
  save_sweep_limits();
  verify_overflow_empty();
}
void CMSParInitialMarkTask::work(uint worker_id) {
  elapsedTimer _timer;
  ResourceMark rm;
  HandleMark   hm;

  // ---------- scan from roots --------------
  _timer.start();
  CMSHeap* heap = CMSHeap::heap();
  ParMarkRefsIntoClosure par_mri_cl(_collector->_span, &(_collector->_markBitMap));

  // ---------- young gen roots --------------
  {
    work_on_young_gen_roots(&par_mri_cl);
    _timer.stop();
    log_trace(gc, task)("Finished young gen initial mark scan work in %dth thread: %3.3f sec", worker_id, _timer.seconds());
  }

  // ---------- remaining roots --------------
  _timer.reset();
  _timer.start();

  CLDToOopClosure cld_closure(&par_mri_cl, true);

  heap->cms_process_roots(_strong_roots_scope,
                          false,     // yg was scanned above
                          GenCollectedHeap::ScanningOption(_collector->CMSCollector::roots_scanning_options()),
                          _collector->should_unload_classes(),
                          &par_mri_cl,
                          &cld_closure,
                          &_par_state_string);

  assert(_collector->should_unload_classes()
         || (_collector->CMSCollector::roots_scanning_options() & GenCollectedHeap::SO_AllCodeCache),
         "if we didn't scan the code cache, we have to be ready to drop nmethods with expired weak oops");
  _timer.stop();
  log_trace(gc, task)("Finished remaining root initial mark scan work in %dth thread: %3.3f sec", worker_id, _timer.seconds());
}

整個過程比較簡單,從 GC Root 出發標記 Old 中的對象,處理完成後藉助 BitMap 處理下 Young 區對 Old 區的引用,整個過程基本都比較快,不多會有較大的停頓。

  • CMS Final Remark 執行步驟,實如今 CMSCollector::checkpointRootsFinalWork() 中,總體代碼和步驟以下:
void CMSCollector::checkpointRootsFinalWork() {
  GCTraceTime(Trace, gc, phases) tm("checkpointRootsFinalWork", _gc_timer_cm);

  assert(haveFreelistLocks(), "must have free list locks");
  assert_lock_strong(bitMapLock());

  ResourceMark rm;
  HandleMark   hm;

  CMSHeap* heap = CMSHeap::heap();

  if (should_unload_classes()) {
    CodeCache::gc_prologue();
  }
  assert(haveFreelistLocks(), "must have free list locks");
  assert_lock_strong(bitMapLock());

  heap->ensure_parsability(false);  // fill TLAB's, but no need to retire them
  // Update the saved marks which may affect the root scans.
  heap->save_marks();

  print_eden_and_survivor_chunk_arrays();

  {
    if (CMSParallelRemarkEnabled) {
      GCTraceTime(Debug, gc, phases) t("Rescan (parallel)", _gc_timer_cm);
      do_remark_parallel();
    } else {
      GCTraceTime(Debug, gc, phases) t("Rescan (non-parallel)", _gc_timer_cm);
      do_remark_non_parallel();
    }
  }
  verify_work_stacks_empty();
  verify_overflow_empty();

  {
    GCTraceTime(Trace, gc, phases) ts("refProcessingWork", _gc_timer_cm);
    refProcessingWork();
  }
  verify_work_stacks_empty();
  verify_overflow_empty();

  if (should_unload_classes()) {
    CodeCache::gc_epilogue();
  }
  JvmtiExport::gc_epilogue();
  assert(_markStack.isEmpty(), "No grey objects");
  size_t ser_ovflw = _ser_pmc_remark_ovflw + _ser_pmc_preclean_ovflw +
                     _ser_kac_ovflw        + _ser_kac_preclean_ovflw;
  if (ser_ovflw > 0) {
    log_trace(gc)("Marking stack overflow (benign) (pmc_pc=" SIZE_FORMAT ", pmc_rm=" SIZE_FORMAT ", kac=" SIZE_FORMAT ", kac_preclean=" SIZE_FORMAT ")",
                         _ser_pmc_preclean_ovflw, _ser_pmc_remark_ovflw, _ser_kac_ovflw, _ser_kac_preclean_ovflw);
    _markStack.expand();
    _ser_pmc_remark_ovflw = 0;
    _ser_pmc_preclean_ovflw = 0;
    _ser_kac_preclean_ovflw = 0;
    _ser_kac_ovflw = 0;
  }
  if (_par_pmc_remark_ovflw > 0 || _par_kac_ovflw > 0) {
     log_trace(gc)("Work queue overflow (benign) (pmc_rm=" SIZE_FORMAT ", kac=" SIZE_FORMAT ")",
                          _par_pmc_remark_ovflw, _par_kac_ovflw);
     _par_pmc_remark_ovflw = 0;
    _par_kac_ovflw = 0;
  }
   if (_markStack._hit_limit > 0) {
     log_trace(gc)(" (benign) Hit max stack size limit (" SIZE_FORMAT ")",
                          _markStack._hit_limit);
   }
   if (_markStack._failed_double > 0) {
     log_trace(gc)(" (benign) Failed stack doubling (" SIZE_FORMAT "), current capacity " SIZE_FORMAT,
                          _markStack._failed_double, _markStack.capacity());
   }
  _markStack._hit_limit = 0;
  _markStack._failed_double = 0;

  if ((VerifyAfterGC || VerifyDuringGC) &&
      CMSHeap::heap()->total_collections() >= VerifyGCStartAt) {
    verify_after_remark();
  }

  _gc_tracer_cm->report_object_count_after_gc(&_is_alive_closure);

  // Change under the freelistLocks.
  _collectorState = Sweeping;
  // Call isAllClear() under bitMapLock
  assert(_modUnionTable.isAllClear(),
      "Should be clear by end of the final marking");
  assert(_ct->cld_rem_set()->mod_union_is_clear(),
      "Should be clear by end of the final marking");
}

Final Remark 是最終的第二次標記,這種狀況只有在 Background GC 執行了 InitialMarking 步驟的情形下才會執行,若是是 Foreground GC 執行的 InitialMarking 步驟則不須要再次執行 FinalRemark。Final Remark 的開始階段與 Init Mark 處理的流程相同,可是後續多了 Card Table 遍歷、Reference 實例的清理並將其加入到 Reference 維護的 pend_list 中,若是要收集元數據信息,還要清理 SystemDictionary、CodeCache、SymbolTable、StringTable 等組件中再也不使用的資源。

4.6.3 策略

知道了兩個 STW 過程執行流程,咱們分析解決就比較簡單了,因爲大部分問題都出在 Final Remark 過程,這裏咱們也拿這個場景來舉例,主要步驟:

  • 【方向】 觀察詳細 GC 日誌,找到出問題時 Final Remark 日誌,分析下 Reference 處理和元數據處理 real 耗時是否正常,詳細信息須要經過 -XX:+PrintReferenceGC 參數開啓。基本在日誌裏面就能定位到大概是哪一個方向出了問題,耗時超過 10% 的就須要關注
2019-02-27T19:55:37.920+0800: 516952.915: [GC (CMS Final Remark) 516952.915: [ParNew516952.939: [SoftReference, 0 refs, 0.0003857 secs]516952.939: [WeakReference, 1362 refs, 0.0002415 secs]516952.940: [FinalReference, 146 refs, 0.0001233 secs]516952.940: [PhantomReference, 0 refs, 57 refs, 0.0002369 secs]516952.940: [JNI Weak Reference, 0.0000662 secs]
[class unloading, 0.1770490 secs]516953.329: [scrub symbol table, 0.0442567 secs]516953.373: [scrub string table, 0.0036072 secs][1 CMS-remark: 1638504K(2048000K)] 1667558K(4352000K), 0.5269311 secs] [Times: user=1.20 sys=0.03, real=0.53 secs]
  • 【根因】 有了具體的方向咱們就能夠進行深刻的分析,通常來講最容易出問題的地方就是 Reference 中的 FinalReference 和元數據信息處理中的 scrub symbol table 兩個階段,想要找到具體問題代碼就須要內存分析工具 MAT 或 JProfiler 了,注意要 dump 即將開始 CMS GC 的堆。在用 MAT 等工具前也能夠先用命令行看下對象 Histogram,有可能直接就能定位問題。

    • 對 FinalReference 的分析主要觀察 java.lang.ref.Finalizer 對象的 dominator tree,找到泄漏的來源。常常會出現問題的幾個點有 Socket 的 SocksSocketImpl 、Jersey 的 ClientRuntime、MySQL 的 ConnectionImpl 等等。
    • scrub symbol table 表示清理元數據符號引用耗時,符號引用是 Java 代碼被編譯成字節碼時,方法在 JVM 中的表現形式,生命週期通常與 Class 一致,當 _should_unload_classes 被設置爲 true 時在 CMSCollector::refProcessingWork() 中與 Class Unload、String Table 一塊兒被處理。
if (should_unload_classes()) {
    {
      GCTraceTime(Debug, gc, phases) t("Class Unloading", _gc_timer_cm);

      // Unload classes and purge the SystemDictionary.
      bool purged_class = SystemDictionary::do_unloading(_gc_timer_cm);

      // Unload nmethods.
      CodeCache::do_unloading(&_is_alive_closure, purged_class);

      // Prune dead klasses from subklass/sibling/implementor lists.
      Klass::clean_weak_klass_links(purged_class);
    }

    {
      GCTraceTime(Debug, gc, phases) t("Scrub Symbol Table", _gc_timer_cm);
      // Clean up unreferenced symbols in symbol table.
      SymbolTable::unlink();
    }

    {
      GCTraceTime(Debug, gc, phases) t("Scrub String Table", _gc_timer_cm);
      // Delete entries for dead interned strings.
      StringTable::unlink(&_is_alive_closure);
    }
  }
  • 【策略】 知道 GC 耗時的根因就比較好處理了,這種問題不會大面積同時爆發,不過有不少時候單臺 STW 的時間會比較長,若是業務影響比較大,及時摘掉流量,具體後續優化策略以下:

    • FinalReference:找到內存來源後經過優化代碼的方式來解決,若是短期沒法定位能夠增長 -XX:+ParallelRefProcEnabled 對 Reference 進行並行處理。
    • symbol table:觀察 MetaSpace 區的歷史使用峯值,以及每次 GC 先後的回收狀況,通常沒有使用動態類加載或者 DSL 處理等,MetaSpace 的使用率上不會有什麼變化,這種狀況能夠經過 -XX:-CMSClassUnloadingEnabled 來避免 MetaSpace 的處理,JDK8 會默認開啓 CMSClassUnloadingEnabled,這會使得 CMS 在 CMS-Remark 階段嘗試進行類的卸載。

4.6.4 小結

正常狀況進行的 Background CMS GC,出現問題基本都集中在 Reference 和 Class 等元數據處理上,在 Reference 類的問題處理方面,無論是 FinalReference,仍是 SoftReference、WeakReference 核心的手段就是找準時機 dump 快照,而後用內存分析工具來分析。Class 處理方面目前除了關閉類卸載開關,沒有太好的方法。

在 G1 中一樣有 Reference 的問題,能夠觀察日誌中的 Ref Proc,處理方法與 CMS 相似。

4.7 場景七:內存碎片&收集器退化

4.7.1 現象

併發的 CMS GC 算法,退化爲 Foreground 單線程串行 GC 模式,STW 時間超長,有時會長達十幾秒。其中 CMS 收集器退化後單線程串行 GC 算法有兩種:

  • 帶壓縮動做的算法,稱爲 MSC,上面咱們介紹過,使用標記-清理-壓縮,單線程全暫停的方式,對整個堆進行垃圾收集,也就是真正意義上的 Full GC,暫停時間要長於普通 CMS。
  • 不帶壓縮動做的算法,收集 Old 區,和普通的 CMS 算法比較類似,暫停時間相對 MSC 算法短一些。

4.7.2 緣由

CMS 發生收集器退化主要有如下幾種狀況:

晉升失敗(Promotion Failed)

顧名思義,晉升失敗就是指在進行 Young GC 時,Survivor 放不下,對象只能放入 Old,但此時 Old 也放不下。直覺上乍一看這種狀況可能會常常發生,但其實由於有 concurrentMarkSweepThread 和擔保機制的存在,發生的條件是很苛刻的,除非是短期將 Old 區的剩餘空間迅速填滿,例如上文中說的動態年齡判斷致使的過早晉升(見下文的增量收集擔保失敗)。另外還有一種狀況就是內存碎片致使的 Promotion Failed,Young GC 覺得 Old 有足夠的空間,結果到分配時,晉級的大對象找不到連續的空間存放。

使用 CMS 做爲 GC 收集器時,運行過一段時間的 Old 區以下圖所示,清除算法致使內存出現多段的不連續,出現大量的內存碎片。

碎片帶來了兩個問題:

  • 空間分配效率較低:上文已經提到過,若是是連續的空間 JVM 能夠經過使用 pointer bumping 的方式來分配,而對於這種有大量碎片的空閒鏈表則須要逐個訪問 freelist 中的項來訪問,查找能夠存放新建對象的地址。
  • 空間利用效率變低:Young 區晉升的對象大小大於了連續空間的大小,那麼將會觸發 Promotion Failed ,即便整個 Old 區的容量是足夠的,但因爲其不連續,也沒法存放新對象,也就是本文所說的問題。

增量收集擔保失敗

分配內存失敗後,會判斷統計獲得的 Young GC 晉升到 Old 的平均大小,以及當前 Young 區已使用的大小也就是最大可能晉升的對象大小,是否大於 Old 區的剩餘空間。只要 CMS 的剩餘空間比前二者的任意一者大,CMS 就認爲晉升仍是安全的,反之,則表明不安全,不進行Young GC,直接觸發Full GC。

顯式 GC

這種狀況參見場景二。

併發模式失敗(Concurrent Mode Failure)

最後一種狀況,也是發生機率較高的一種,在 GC 日誌中常常能看到 Concurrent Mode Failure 關鍵字。這種是因爲併發 Background CMS GC 正在執行,同時又有 Young GC 晉升的對象要放入到了 Old 區中,而此時 Old 區空間不足形成的。

爲何 CMS GC 正在執行還會致使收集器退化呢?主要是因爲 CMS 沒法處理浮動垃圾(Floating Garbage)引發的。CMS 的併發清理階段,Mutator 還在運行,所以不斷有新的垃圾產生,而這些垃圾不在此次清理標記的範疇裏,沒法在本次 GC 被清除掉,這些就是浮動垃圾,除此以外在 Remark 以前那些斷開引用脫離了讀寫屏障控制的對象也算浮動垃圾。因此 Old 區回收的閾值不能過高,不然預留的內存空間極可能不夠,從而致使 Concurrent Mode Failure 發生。

4.7.3 策略

分析到具體緣由後,咱們就能夠針對性解決了,具體思路仍是從根因出發,具體解決策略:

  • 內存碎片: 經過配置 -XX:UseCMSCompactAtFullCollection=true 來控制 Full GC的過程當中是否進行空間的整理(默認開啓,注意是Full GC,不是普通CMS GC),以及 -XX: CMSFullGCsBeforeCompaction=n 來控制多少次 Full GC 後進行一次壓縮。
  • 增量收集: 下降觸發 CMS GC 的閾值,即參數 -XX:CMSInitiatingOccupancyFraction 的值,讓 CMS GC 儘早執行,以保證有足夠的連續空間,也減小 Old 區空間的使用大小,另外須要使用 -XX:+UseCMSInitiatingOccupancyOnly 來配合使用,否則 JVM 僅在第一次使用設定值,後續則自動調整。
  • 浮動垃圾: 視狀況控制每次晉升對象的大小,或者縮短每次 CMS GC 的時間,必要時可調節 NewRatio 的值。另外就是使用 -XX:+CMSScavengeBeforeRemark 在過程當中提早觸發一次 Young GC,防止後續晉升過多對象。

4.7.4 小結

正常狀況下觸發併發模式的 CMS GC,停頓很是短,對業務影響很小,但 CMS GC 退化後,影響會很是大,建議發現一次後就完全根治。只要能定位到內存碎片、浮動垃圾、增量收集相關等具體產生緣由,仍是比較好解決的,關於內存碎片這塊,若是 -XX:CMSFullGCsBeforeCompaction 的值很差選取的話,能夠使用 -XX:PrintFLSStatistics 來觀察內存碎片率狀況,而後再設置具體的值。

最後就是在編碼的時候也要避免須要連續地址空間的大對象的產生,如過長的字符串,用於存放附件、序列化或反序列化的 byte 數組等,還有就是過早晉升問題儘可能在爆發問題前就避免掉。

4.8 場景八:堆外內存 OOM

4.8.1 現象

內存使用率不斷上升,甚至開始使用 SWAP 內存,同時可能出現 GC 時間飆升,線程被 Block 等現象,經過 top 命令發現 Java 進程的 RES 甚至超過了 -Xmx 的大小。出現這些現象時,基本能夠肯定是出現了堆外內存泄漏。

4.8.2 緣由

JVM 的堆外內存泄漏,主要有兩種的緣由:

  • 經過 UnSafe#allocateMemoryByteBuffer#allocateDirect 主動申請了堆外內存而沒有釋放,常見於 NIO、Netty 等相關組件。
  • 代碼中有經過 JNI 調用 Native Code 申請的內存沒有釋放。

4.8.3 策略

哪一種緣由形成的堆外內存泄漏?

首先,咱們須要肯定是哪一種緣由致使的堆外內存泄漏。這裏能夠使用 NMT(NativeMemoryTracking) 進行分析。在項目中添加 -XX:NativeMemoryTracking=detail JVM參數後重啓項目(須要注意的是,打開 NMT 會帶來 5%~10% 的性能損耗)。使用命令 jcmd pid VM.native_memory detail 查看內存分佈。重點觀察 total 中的 committed,由於 jcmd 命令顯示的內存包含堆內內存、Code 區域、經過 Unsafe.allocateMemoryDirectByteBuffer 申請的內存,可是不包含其餘 Native Code(C 代碼)申請的堆外內存。

若是 total 中的 committed 和 top 中的 RES 相差不大,則應爲主動申請的堆外內存未釋放形成的,若是相差較大,則基本能夠肯定是 JNI 調用形成的。

緣由一:主動申請未釋放

JVM 使用 -XX:MaxDirectMemorySize=size 參數來控制可申請的堆外內存的最大值。在 Java8 中,若是未配置該參數,默認和 -Xmx 相等。

NIO 和 Netty 都會取 -XX:MaxDirectMemorySize 配置的值,來限制申請的堆外內存的大小。NIO 和 Netty 中還有一個計數器字段,用來計算當前已申請的堆外內存大小,NIO 中是 java.nio.Bits#totalCapacity、Netty 中 io.netty.util.internal.PlatformDependent#DIRECT_MEMORY_COUNTER

當申請堆外內存時,NIO 和 Netty 會比較計數器字段和最大值的大小,若是計數器的值超過了最大值的限制,會拋出 OOM 的異常。

NIO 中是:OutOfMemoryError: Direct buffer memory

Netty 中是:OutOfDirectMemoryError: failed to allocate capacity byte(s) of direct memory (used: usedMemory , max: DIRECT_MEMORY_LIMIT )

咱們能夠檢查代碼中是如何使用堆外內存的,NIO 或者是 Netty,經過反射,獲取到對應組件中的計數器字段,並在項目中對該字段的數值進行打點,便可準確地監控到這部分堆外內存的使用狀況。

此時,能夠經過 Debug 的方式肯定使用堆外內存的地方是否正確執行了釋放內存的代碼。另外,須要檢查 JVM 的參數是否有 -XX:+DisableExplicitGC 選項,若是有就去掉,由於該參數會使 System.gc 失效。(場景二:顯式 GC 的去與留)

緣由二:經過 JNI 調用的 Native Code 申請的內存未釋放

這種狀況排查起來比較困難,咱們能夠經過 Google perftools + Btrace 等工具,幫助咱們分析出問題的代碼在哪裏。

gperftools 是 Google 開發的一款很是實用的工具集,它的原理是在 Java 應用程序運行時,當調用 malloc 時換用它的 libtcmalloc.so,這樣就能對內存分配狀況作一些統計。咱們使用 gperftools 來追蹤分配內存的命令。以下圖所示,經過 gperftools 發現 Java_java_util_zip_Inflater_init 比較可疑。

接下來能夠使用 Btrace,嘗試定位具體的調用棧。Btrace 是 Sun 推出的一款 Java 追蹤、監控工具,能夠在不停機的狀況下對線上的 Java 程序進行監控。以下圖所示,經過 Btrace 定位出項目中的 ZipHelper 在頻繁調用 GZIPInputStream ,在堆外內存分配對象。

最終定位到是,項目中對 GIPInputStream 的使用錯誤,沒有正確的 close()。

除了項目自己的緣由,還可能有外部依賴致使的泄漏,如 Netty 和 Spring Boot,詳細狀況能夠學習下這兩篇文章,Spring Boot引發的「堆外內存泄漏」排查及經驗總結Netty堆外內存泄露排查盛宴

4.8.4 小結

首先能夠使用 NMT + jcmd 分析泄漏的堆外內存是哪裏申請,肯定緣由後,使用不一樣的手段,進行緣由定位。

4.9 場景九:JNI 引起的 GC 問題

4.9.1 現象

在 GC 日誌中,出現 GC Cause 爲 GCLocker Initiated GC。

2020-09-23T16:49:09.727+0800: 504426.742: [GC (GCLocker Initiated GC) 504426.742: [ParNew (promotion failed): 209716K->6042K(1887488K), 0.0843330 secs] 1449487K->1347626K(3984640K), 0.0848963 secs] [Times: user=0.19 sys=0.00, real=0.09 secs]
2020-09-23T16:49:09.812+0800: 504426.827: [Full GC (GCLocker Initiated GC) 504426.827: [CMS: 1341583K->419699K(2097152K), 1.8482275 secs] 1347626K->419699K(3984640K), [Metaspace: 297780K->297780K(1329152K)], 1.8490564 secs] [Times: user=1.62 sys=0.20, real=1.85 secs]

4.9.2 緣由

JNI(Java Native Interface)意爲 Java 本地調用,它容許 Java 代碼和其餘語言寫的 Native 代碼進行交互。

JNI 若是須要獲取 JVM 中的 String 或者數組,有兩種方式:

  • 拷貝傳遞。
  • 共享引用(指針),性能更高。

因爲 Native 代碼直接使用了 JVM 堆區的指針,若是這時發生 GC,就會致使數據錯誤。所以,在發生此類 JNI 調用時,禁止 GC 的發生,同時阻止其餘線程進入 JNI 臨界區,直到最後一個線程退出臨界區時觸發一次 GC。

GC Locker 實驗:

public class GCLockerTest {

  static final int ITERS = 100;
  static final int ARR_SIZE =  10000;
  static final int WINDOW = 10000000;

  static native void acquire(int[] arr);
  static native void release(int[] arr);

  static final Object[] window = new Object[WINDOW];

  public static void main(String... args) throws Throwable {
    System.loadLibrary("GCLockerTest");
    int[] arr = new int[ARR_SIZE];

    for (int i = 0; i < ITERS; i++) {
      acquire(arr);
      System.out.println("Acquired");
      try {
        for (int c = 0; c < WINDOW; c++) {
          window[c] = new Object();
        }
      } catch (Throwable t) {
        // omit
      } finally {
        System.out.println("Releasing");
        release(arr);
      }
    }
  }
}

#include <jni.h>
#include "GCLockerTest.h"

static jbyte* sink;

JNIEXPORT void JNICALL Java_GCLockerTest_acquire(JNIEnv* env, jclass klass, jintArray arr) {
sink = (*env)->GetPrimitiveArrayCritical(env, arr, 0);
}

JNIEXPORT void JNICALL Java_GCLockerTest_release(JNIEnv* env, jclass klass, jintArray arr) {
(*env)->ReleasePrimitiveArrayCritical(env, arr, sink, 0);
}

運行該 JNI 程序,能夠看到發生的 GC 都是 GCLocker Initiated GC,而且注意在 「Acquired」 和 「Released」 時不可能發生 GC。

GC Locker 可能致使的不良後果有:

  • 若是此時是 Young 區不夠 Allocation Failure 致使的 GC,因爲沒法進行 Young GC,會將對象直接分配至 Old 區。
  • 若是 Old 區也沒有空間了,則會等待鎖釋放,致使線程阻塞。
  • 可能觸發額外沒必要要的 Young GC,JDK 有一個 Bug,有必定的概率,原本只該觸發一次 GCLocker Initiated GC 的 Young GC,實際發生了一次 Allocation Failure GC 又緊接着一次 GCLocker Initiated GC。是由於 GCLocker Initiated GC 的屬性被設爲 full,致使兩次 GC 不能收斂。

4.9.3 策略

  • 添加 -XX+PrintJNIGCStalls 參數,能夠打印出發生 JNI 調用時的線程,進一步分析,找到引起問題的 JNI 調用。
  • JNI 調用須要謹慎,不必定能夠提高性能,反而可能形成 GC 問題。
  • 升級 JDK 版本到 14,避免 JDK-8048556 致使的重複 GC。

4.9.4 小結

JNI 產生的 GC 問題較難排查,須要謹慎使用。

5. 總結

在這裏,咱們把整個文章內容總結一下,方便你們總體地理解回顧。

5.1 處理流程(SOP)

下圖爲總體 GC 問題普適的處理流程,重點的地方下面會單獨標註,其餘的基本都是標準處理流程,此處再也不贅述,最後在整個問題都處理完以後有條件的話建議作一下覆盤。

  • 制定標準: 這塊內容其實很是重要,但大部分系統都是缺失的,筆者過往面試的同窗中只有不到一成的同窗能給出本身的系統 GC 標準到底什麼樣,其餘的都是用的統一指標模板,缺乏預見性,具體指標制定能夠參考 3.1 中的內容,須要結合應用系統的 TP9999 時間和延遲、吞吐量等設定具體的指標,而不是被問題驅動。
  • 保留現場: 目前線上服務基本都是分佈式服務,某個節點發生問題後,若是條件容許必定不要直接操做重啓、回滾等動做恢復,優先經過摘掉流量的方式來恢復,這樣咱們能夠將堆、棧、GC 日誌等關鍵信息保留下來,否則錯過了定位根因的時機,後續解決難度將大大增長。固然除了這些,應用日誌、中間件日誌、內核日誌、各類 Metrics 指標等對問題分析也有很大幫助。
  • 因果分析: 判斷 GC 異常與其餘系統指標異常的因果關係,能夠參考筆者在 3.2 中介紹的時序分析、機率分析、實驗分析、反證分析等 4 種因果分析法,避免在排查過程當中走入誤區。
  • 根因分析: 確實是 GC 的問題後,能夠藉助上文提到的工具並經過 5 why 根因分析法以及跟第三節中的九種常見的場景進行逐一匹配,或者直接參考下文的根因魚骨圖,找出問題發生根因,最後再選擇優化手段。

5.2 根因魚骨圖

送上一張問題根因魚骨圖,通常狀況下咱們在處理一個 GC 問題時,只要能定位到問題的「病竈」,有的放矢,其實就至關於解決了 80%,若是在某些場景下不太好定位,你們能夠藉助這種根因分析圖經過排除法去定位。

5.3 調優建議

  • Trade Off: 與 CAP 註定要缺一角同樣,GC 優化要在延遲(Latency)、吞吐量(Throughput)、容量(Capacity)三者之間進行權衡。
  • 最終手段: GC 發生問題不是必定要對 JVM 的 GC 參數進行調優,大部分狀況下是經過 GC 的狀況找出一些業務問題,切記上來就對 GC 參數進行調整,固然有明確配置錯誤的場景除外。
  • 控制變量: 控制變量法是在蒙特卡洛(Monte Carlo)方法中用於減小方差的一種技術方法,咱們調優的時候儘可能也要使用,每次調優過程儘量只調整一個變量。
  • 善用搜索: 理論上 99.99% 的 GC 問題基本都被遇到了,咱們要學會使用搜索引擎的高級技巧,重點關注 StackOverFlow、Github 上的 Issue、以及各類論壇博客,先看看其餘人是怎麼解決的,會讓解決問題事半功倍。能看到這篇文章,你的搜索能力基本過關了~
  • 調優重點: 整體上來說,咱們開發的過程當中遇到的問題類型也基本都符合正態分佈,太簡單或太複雜的基本遇到的機率很低,筆者這裏將中間最重要的三個場景添加了「*」標識,但願閱讀完本文以後能夠觀察下本身負責的系統,是否存在上述問題。
  • GC 參數: 若是堆、棧確實沒法第一時間保留,必定要保留 GC 日誌,這樣咱們最起碼能夠看到 GC Cause,有一個大概的排查方向。關於 GC 日誌相關參數,最基本的 -XX:+HeapDumpOnOutOfMemoryError 等一些參數就再也不提了,筆者建議添加如下參數,能夠提升咱們分析問題的效率。

  • 其餘建議: 上文場景中沒有提到,可是對 GC 性能也有提高的一些建議。

    • 主動式 GC: 也有另開生面的作法,經過監控手段監控觀測 Old 區的使用狀況,即將到達閾值時將應用服務摘掉流量,手動觸發一次 Major GC,減小 CMS GC 帶來的停頓,但隨之系統的健壯性也會減小,如非必要不建議引入。
    • 禁用偏向鎖: 偏向鎖在只有一個線程使用到該鎖的時候效率很高,可是在競爭激烈狀況會升級成輕量級鎖,此時就須要先消除偏向鎖,這個過程是 STW 的。若是每一個同步資源都走這個升級過程,開銷會很是大,因此在已知併發激烈的前提下,通常會禁用偏向鎖 -XX:-UseBiasedLocking 來提升性能。
    • 虛擬內存: 啓動初期有些操做系統(例如 Linux)並無真正分配物理內存給 JVM ,而是在虛擬內存中分配,使用的時候纔會在物理內存中分配內存頁,這樣也會致使 GC 時間較長。這種狀況能夠添加 -XX:+AlwaysPreTouch 參數,讓 VM 在 commit 內存時跑個循環來強制保證申請的內存真的 commit,避免運行時觸發缺頁異常。在一些大內存的場景下,有時候能將前幾回的 GC 時間降一個數量級,可是添加這個參數後,啓動的過程可能會變慢。

6. 寫在最後

最後,再說筆者我的的一些小建議,遇到一些 GC 問題,若是有精力,必定要探本窮源,找出最深層次的緣由。另外,在這個信息氾濫的時代,有一些被「奉爲圭臬」的經驗可能都是錯誤的,儘可能養成看源碼的習慣,有一句話說到「源碼面前,了無祕密」,也就意味着遇到搞不懂的問題,咱們能夠從源碼中一窺究竟,某些場景下確有奇效。但也不是隻靠讀源碼來學習,若是硬啃源碼但不理會其背後可能蘊含的理論基礎,那很容易「撿芝麻丟西瓜」,「只見樹木,不見森林」,讓「了無祕密」變成了一句空話,咱們仍是要結合一些實際的業務場景去針對性地學習。

你的時間在哪裏,你的成就就會在哪裏。筆者也是在前兩年纔開始逐步地在 GC 方向上不斷深刻,查問題、看源碼、作總結,每一個 Case 造成一個小的閉環,目前初步摸到了 GC 問題處理的一些門道,同時將經驗總結應用於生產環境實踐,慢慢地造成一個良性循環。

本篇文章主要是介紹了 CMS GC 的一些常見場景分析,另一些,如 CodeCache 問題致使 JIT 失效、SafePoint 就緒時間長、Card Table 掃描耗時等問題不太常見就沒有花太多篇幅去講解。Java GC 是在「分代」的思想下內捲了不少年才突破到了「分區」,目前在美團也已經開始使用 G1 來替換使用了多年的 CMS,雖然在小的堆方面 G1 還略遜色於 CMS,但這是一個趨勢,短期沒法升級到 ZGC,因此將來遇到的 G1 的問題可能會逐漸增多。目前已經收集到 Remember Set 粗化、Humongous 分配、Ergonomics 異常、Mixed GC 中 Evacuation Failure 等問題,除此以外也會給出 CMS 升級到 G1 的一些建議,接下來筆者將繼續完成這部分文章整理,敬請期待。

「防火」永遠要勝於「救火」,不放過任何一個異常的小指標(通常來講,任何不平滑的曲線都是值得懷疑的) ,就有可能避免一次故障的發生。做爲 Java 程序員基本都會遇到一些 GC 的問題,獨立解決 GC 問題是咱們必須邁過的一道坎。開篇中也提到過 GC 做爲經典的技術,很是值得咱們學習,一些 GC 的學習材料,如《The Garbage Collection Handbook》《深刻理解Java虛擬機》等也是常讀常新,趕忙動起來,苦練 GC 基本功吧。

最後的最後,再多囉嗦一句,目前全部 GC 調優相關的文章,第一句講的就是「不要過早優化」,使得不少同窗對 GC 優化望而卻步。在這裏筆者提出不同的觀點,熵增定律(在一個孤立系統裏,若是沒有外力作功,其總混亂度(即熵)會不斷增大)在計算機系統一樣適用,若是不主動作功使熵減,系統終究會脫離你的掌控,在咱們對業務系統和 GC 原理掌握得足夠深的時候,能夠放心大膽地作優化,由於咱們基本能夠預測到每個操做的結果,放手一搏吧,少年!

7. 參考資料

8. 做者簡介

  • 新宇:2015 年加入美團,到店住宿門票業務開發工程師。
  • 湘銘:2018 年加入美團,到店客戶平臺開發工程師。
  • 祥璞:2018 年加入美團,到店客戶平臺開發工程師。

9. 招聘信息

美團到店事業羣住宿門票數據智能組誠招小夥伴,從供、控、選、售等層面全方位提高業務競爭力,十萬級 QPS 處理,億級數據分析,完整業務閉環,目前有海量 HC,有興趣的請將郵件發送至 hezhiming@meituan.com,咱們會在第一時間與你聯繫。

想閱讀更多技術文章,請關注美團技術團隊(meituantech)官方微信公衆號。

相關文章
相關標籤/搜索