你看懂 Elasticsearch Log 中的 GC 日誌了嗎?

若是你關注過 elasticsearch 的日誌,可能會看到以下相似的內容:java

[2018-06-30T17:57:23,848][WARN ][o.e.m.j.JvmGcMonitorService] [qoo--eS] [gc][228384] overhead, spent [2.2s] collecting in the last [2.3s]

[2018-06-30T17:57:29,020][INFO ][o.e.m.j.JvmGcMonitorService] [qoo--eS] [gc][old][228385][160772] duration [5s], collections [1]/[5.1s], total [5s]/[4.4d], memory [945.4mb]->[958.5mb]/[1007.3mb], all_pools {[young] [87.8mb]->[100.9mb]/[133.1mb]}{[survivor] [0b]->[0b]/[16.6mb]}{[old] [857.6mb]->[857.6mb]/[857.6mb]}

看到其中的[gc]關鍵詞你也猜到了這是與 GC 相關的日誌,那麼你瞭解每一部分的含義嗎?若是不瞭解,你能夠繼續往下看了。算法

咱們先從最簡單的看起:編程

  1. 第一部分是日誌發生的時間
  2. 第二部分是日誌級別,這裏分別是WARNINFO
  3. 第三部分是輸出日誌的類,咱們後面也會講到這個類
  4. 第四部分是當前 ES 節點名稱
  5. 第五部分是 gc 關鍵詞,咱們就從這個關鍵詞聊起。

友情提示:對 GC 已經瞭如指掌的同窗,能夠直接翻到最後看答案。數據結構

1. 什麼是 GC?

GC,全稱是 Garbage Collection (垃圾收集)或者 Garbage Collector(垃圾收集器)。多線程

在使用 C語言編程的時候,咱們要手動的經過 mallocfree來申請和釋放數據須要的內存,若是忘記釋放內存,就會發生內存泄露的狀況,即無用的數據佔用了寶貴的內存資源。而Java 語言編程不須要顯示的申請和釋放內存,由於 JVM 能夠自動管理內存,這其中最重要的一部分就是 GC,即 JVM 能夠自主地去釋放無用數據(垃圾)佔用的內存。併發

咱們研究 GC 的主要緣由是 GC 的過程會有 Stop The World(STW)的狀況發生,即此時用戶線程會中止工做,若是 STW 的時間過長,則應用的可用性、實時性等就降低的很厲害。jvm

GC主要解決以下3個問題:elasticsearch

  1. 如何找到垃圾?
  2. 如何回收垃圾?
  3. 什麼時候回收垃圾?

咱們一個個來看下。spa

1.1 如何找到垃圾?

所謂垃圾,指的是再也不被使用(引用)的對象。Java 的對象都是在堆(Heap)上建立的,咱們這裏默認也只討論堆。那麼如今問題就變爲如何斷定一個對象是否還有被引用,思路主要有以下兩種:線程

  1. 引用計數法,即在對象被引用時加1,去除引用時減1,若是引用值爲0,即代表該對象可回收了。
  2. 可達性分析法,即經過遍歷已知的存活對象(GC Roots)的引用鏈來標記出全部存活對象

方法1簡單粗暴效率高,但準確度不行,尤爲是面對互相引用的垃圾對象時無能爲力。

方法2是目前經常使用的方法,這裏有一個關鍵是 GC Roots,它是斷定的源頭,感興趣的同窗能夠本身去研究下,這裏就不展開講了。

1.2 如何回收垃圾?

垃圾找到了,該怎麼回收呢?看起來彷佛是個很傻的問題。直接收起來扔掉不就行了?!對應到程序的操做,就是直接將這些對象佔用的空間標記爲空閒不就行了嗎?那咱們就來看一下這個基礎的回收算法:標記-清除(Mark-Sweep)算法。

1.2.1 標記-清除 算法(Mark Sweep)

該算法很簡單,使用經過可達性分析分析方法標記出垃圾,而後直接回收掉垃圾區域。它的一個顯著問題是一段時間後,內存會出現大量碎片,致使雖然碎片總和很大,但沒法知足一個大對象的內存申請,從而致使 OOM,而過多的內存碎片(須要相似鏈表的數據結構維護),也會致使標記和清除的操做成本高,效率低下,以下圖所示:

1.2.2 複製算法(Copying)

爲了解決上面算法的效率問題,有人提出了複製算法。它將可用內存一分爲二,每次只用一塊,當這一塊內存不夠用時,便觸發 GC,將當前存活對象複製(Copy)到另外一塊上,以此往復。這種算法高效的緣由在於分配內存時只須要將指針後移,不須要維護鏈表等。但它最大的問題是對內存的浪費,使用率只有 50%。

但這種算法在一種狀況下會很高效:Java 對象的存活時間極短。據 IBM 研究,Java 對象高達 98% 是朝生夕死的,這也意味着每次 GC 能夠回收大部分的內存,須要複製的數據量也很小,這樣它的執行效率就會很高。

1.2.3 標記-整理算法(Mark Compact)

該算法解決了第1中算法的內存碎片問題,它會在回收階段將全部內存作整理,以下圖所示:

但它的問題也在於增長了整理階段,也就增長了 GC 的時間。

1.2.4 分代收集算法(Generation Collection)

既然大部分 Java 對象是朝生夕死的,那麼咱們將內存按照 Java 生存時間分爲 新生代(Young)老年代(Old),前者存放短命僧,後者存放長壽佛,固然長壽佛也是由短命僧升級上來的。而後針對二者能夠採用不一樣的回收算法,好比對於新生代採用複製算法會比較高效,而對老年代能夠採用標記-清除或者標記-整理算法。這種算法也是最經常使用的。JVM Heap 分代後的劃分通常以下所示,新生代通常會分爲 Eden、Survivor0、Survivor1區,便於使用複製算法。

將內存分代後的 GC 過程通常相似下圖所示:

  1. 對象通常都是先在 Eden區建立
  2. Eden區滿,觸發 Young GC,此時將 Eden中還存活的對象複製到 S0中,並清空 Eden區後繼續爲新的對象分配內存
  3. Eden區再次滿後,觸發又一次的 Young GC,此時會將 EdenS0中存活的對象複製到 S1中,而後清空EdenS0後繼續爲新的對象分配內存
  4. 每通過一次 Young GC,存活下來的對象都會將本身存活次數加1,當達到必定次數後,會隨着一次 Young GC 晉升到 Old
  5. Old區也會在合適的時機進行本身的 GC

1.2.5 常見的垃圾收集器

前面咱們講了衆多的垃圾收集算法,那麼其具體的實現就是垃圾收集器,也是咱們實際使用中會具體用到的。現代的垃圾收集機制基本都是分代收集算法,而 YoungOld區分別有不一樣的垃圾收集器,簡單總結以下圖:

從上圖咱們能夠看到 YoungOld區有不一樣的垃圾收集器,實際使用時會搭配使用,也就是上圖中兩兩連線的收集器是能夠搭配使用的。這些垃圾收集器按照運行原理大概能夠分爲以下幾類:

  • Serial GC串行,單線程的收集器,運行 GC 時須要中止全部的用戶線程,且只有一個 GC 線程
  • Parallel GC並行,多線程的收集器,是 Serial 的多線程版,運行時也須要中止全部用戶線程,但同時運行多個 GC 線程,因此效率高一些
  • Concurrent GC併發,多線程收集器,GC 分多階段執行,部分階段容許用戶線程與 GC 線程同時運行,這也就是併發的意思,你們要和並行作一個區分。
  • 其餘

咱們下面簡單看一下他們的運行機制。

1.2.5.1 Serial GC

該類 Young區的爲 Serial GCOld區的爲Serial Old GC。執行大體以下所示:

1.2.5.2 Parallel GC

該類Young 區的有 ParNewParallel ScavengeOld 區的有Parallel Old。其運行機制以下,相比 Serial GC ,其最大特色在於 GC 線程是並行的,效率高不少:

1.2.5.3 Concurrent Mark-Sweep GC

該類目前只是針對 Old 區,最多見就是CMS GC,它的執行分爲多個階段,只有部分階段須要中止用戶進程,這裏不詳細介紹了,感興趣能夠去找相關文章來看,大致執行以下:

1.2.5.4 其餘

目前最新的 GC 有G1GCZGC,其運行機制與上述均不相同,雖然他們也是分代收集算法,但會把 Heap 分紅多個 region 來作處理,這裏不展開講,感興趣的能夠參看最後參考資料的內容。

1.2.6 Elasticsearch 的 GC 組合

Elasticsearch 默認的 GC 配置是CMS GC ,其 Young 區ParNewOld 區CMS,你們能夠在 config/jvm.options中看到以下的配置:

## GC configuration
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly

1.3 什麼時候進行回收?

如今咱們已經知道如何找到和回收垃圾了,那麼何時回收呢?簡單總結以下:

  1. Young 區的GC 都是在 Eden 區滿時觸發
  2. Serial Old 和 Parallel Old 在 Old 區是在 Young GC 時預測Old 區是否能夠爲 young 區 promote 到 old 區 的 object 分配空間,若是不可用則觸發 Old GC。這個也能夠理解爲是 Old區滿時。
  3. CMS GC 是在 Old 區大小超過必定比例後觸發,而不是 Old 區滿。這個緣由在於 CMS GC 是併發的算法,也就是說在 GC 線程收集垃圾的時候,用戶線程也在運行,所以須要預留一些 Heap 空間給用戶線程使用,防止因爲沒法分配空間而致使 Full GC 發生。

2. GC Log 如何閱讀?

前面講了這麼多,終於能夠回到開篇的問題了,咱們直接來看答案

[2018-06-30T17:57:23,848][WARN ][o.e.m.j.JvmGcMonitorService] [qoo--eS] [gc][228384] overhead, spent [2.2s] collecting in the last [2.3s]

[gc][這是第228384次GC 檢查] 在最近 2.3 s 內花了 2.2s 用來作垃圾收集,這佔比彷佛有些過了,請抓緊來關注下。

[2018-06-30T17:57:29,020][INFO ][o.e.m.j.JvmGcMonitorService] [qoo--eS] [gc][old][228385][160772] duration [5s], collections [1]/[5.1s], total [5s]/[4.4d], memory [945.4mb]->[958.5mb]/[1007.3mb], all_pools {[young] [87.8mb]->[100.9mb]/[133.1mb]}{[survivor] [0b]->[0b]/[16.6mb]}{[old] [857.6mb]->[857.6mb]/[857.6mb]}

咱們直接來看具體的含義好了,相信有了前面的 GC 基礎知識,你們在看這裏解釋的時候就很是清楚了。

  • [gc][本次是 old GC][這是第228385次 GC 檢查][從 JVM 啓動至今發生的第 160772次 GC]
  • duration [本次檢查到的 GC 總耗時 5 秒,多是屢次的加和],
  • collections [從上次檢查至今總共發生1次 GC]/[從上次檢查至今已過去 5.1 秒],
  • total [本次檢查到的 GC 總耗時爲 5 秒]/[從 JVM 啓動至今發生的 GC 總耗時爲 4.4 天],
  • memory [ GC 前 Heap memory 空間]->[GC 後 Heap memory 空間]/[Heap memory 總空間],
  • all_pools(分代部分的詳情) {[young 區][GC 前 Memory ]->[GC後 Memory]/[young區 Memory 總大小] } {[survivor 區][GC 前 Memory ]->[GC後 Memory]/[survivor區 Memory 總大小] }{[old 區][GC 前 Memory ]->[GC後 Memory]/[old區 Memory 總大小] }

3. 看看源碼

從日誌中咱們能夠看到輸出這些日誌的類名叫作JvmGcMonitorService,咱們去源碼中搜索很快會找到它/Users/rockybean/code/elasticsearch/core/src/main/java/org/elasticsearch/monitor/jvm/JvmGcMonitorService.java,這裏就不詳細展開講解源碼了,它執行的內容大概以下圖所示:

關於打印日誌的格式在源碼也有,以下所示:

private static final String SLOW_GC_LOG_MESSAGE =
"[gc][{}][{}][{}] duration [{}], collections [{}]/[{}], total [{}]/[{}], memory [{}]->[{}]/[{}], all_pools {}";
private static final String OVERHEAD_LOG_MESSAGE = "[gc][{}] overhead, spent [{}] collecting in the last [{}]";

另外細心的同窗會發現輸出的日誌中 gc 只分了 young 和 old ,緣由在於 ES 對 GC Name 作了封裝,封裝的類爲:org.elasticsearch.monitor.jvm.GCNames,相關代碼以下:

public static String getByMemoryPoolName(String poolName, String defaultName) {
        if ("Eden Space".equals(poolName) || "PS Eden Space".equals(poolName) || "Par Eden Space".equals(poolName) || "G1 Eden Space".equals(poolName)) {
            return YOUNG;
        }
        if ("Survivor Space".equals(poolName) || "PS Survivor Space".equals(poolName) || "Par Survivor Space".equals(poolName) || "G1 Survivor Space".equals(poolName)) {
            return SURVIVOR;
        }
        if ("Tenured Gen".equals(poolName) || "PS Old Gen".equals(poolName) || "CMS Old Gen".equals(poolName) || "G1 Old Gen".equals(poolName)) {
            return OLD;
        }
        return defaultName;
    }

    public static String getByGcName(String gcName, String defaultName) {
        if ("Copy".equals(gcName) || "PS Scavenge".equals(gcName) || "ParNew".equals(gcName) || "G1 Young Generation".equals(gcName)) {
            return YOUNG;
        }
        if ("MarkSweepCompact".equals(gcName) || "PS MarkSweep".equals(gcName) || "ConcurrentMarkSweep".equals(gcName) || "G1 Old Generation".equals(gcName)) {
            return OLD;
        }
        return defaultName;
    }

在上面的代碼中,你會看到不少咱們在上一節中提到的 GC 算法的名稱。

至此,源碼相關部分也講解完畢,感興趣的你們能夠自行去查閱。

4. 總結

講解 GC 的文章已經不少,本文又嘮嘮叨叨地講一遍基礎知識,是但願對於第一次瞭解 GC 的同窗有所幫助。由於只有瞭解了這些基礎知識,你纔不至於被這些 GC 的輸出嚇懵。但願本文對你理解 ES 的 GC 日誌 有所幫助。

5. 參考資料

  1. Java Hotspot G1 GC的一些關鍵技術(https://mp.weixin.qq.com/s/4ufdCXCwO56WAJnzng_-ow
  2. Understanding Java Garbage Collection(https://www.cubrid.org/blog/understanding-java-garbage-collection
  3. 《深刻理解Java虛擬機:JVM高級特性與最佳實踐》

6. 相關推薦

若是你想深刻的瞭解 JAVA GC 的知識,能夠關注 ElasticTalk 公衆號,回覆 GC關鍵詞後便可獲取做者推薦的電子書等資料。

elasticTalk,qrcode

相關文章
相關標籤/搜索