你看懂 Elasticsearch Log 中的 GC 日志了嗎?【轉(zhuǎn)載】

你看懂 Elasticsearch Log 中的 GC 日志了嗎?[轉(zhuǎn)載]

如果你關注過 elasticsearch 的日志,可能會看到如下類似的內(nèi)容:

[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 相關的日志,那么你了解每一部分的含義嗎?如果不了解,你可以繼續(xù)往下看了。

我們先從最簡單的看起:

  1. 第一部分是日志發(fā)生的時間
  2. 第二部分是日志級別,這里分別是WARNINFO
  3. 第三部分是輸出日志的類,我們后面也會講到這個類
  4. 第四部分是當前 ES 節(jié)點名稱
  5. 第五部分是 gc 關鍵詞,我們就從這個關鍵詞聊起。

友情提示:對 GC 已經(jīng)了如指掌的同學,可以直接翻到最后看答案。

1. 什么是 GC?

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

在使用 C語言編程的時候,我們要手動的通過 mallocfree來申請和釋放數(shù)據(jù)需要的內(nèi)存,如果忘記釋放內(nèi)存,就會發(fā)生內(nèi)存泄露的情況,即無用的數(shù)據(jù)占用了寶貴的內(nèi)存資源。而Java 語言編程不需要顯示的申請和釋放內(nèi)存,因為 JVM 可以自動管理內(nèi)存,這其中最重要的一部分就是 GC,即 JVM 可以自主地去釋放無用數(shù)據(jù)(垃圾)占用的內(nèi)存。

我們研究 GC 的主要原因是 GC 的過程會有 Stop The World(STW)的情況發(fā)生,即此時用戶線程會停止工作,如果 STW 的時間過長,則應用的可用性、實時性等就下降的很厲害。

GC主要解決如下3個問題:

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

我們一個個來看下。

1.1 如何找到垃圾?

所謂垃圾,指的是不再被使用(引用)的對象。Java 的對象都是在堆(Heap)上創(chuàng)建的,我們這里默認也只討論堆。那么現(xiàn)在問題就變?yōu)槿绾闻卸ㄒ粋€對象是否還有被引用,思路主要有如下兩種:

  1. 引用計數(shù)法,即在對象被引用時加1,去除引用時減1,如果引用值為0,即表明該對象可回收了。
  2. 可達性分析法,即通過遍歷已知的存活對象(GC Roots)的引用鏈來標記出所有存活對象

方法2是目前常用的方法,這里有一個關鍵是 GC Roots,它是判定的源頭,感興趣的同學可以自己去研究下,這里就不展開講了。

image.png

1.2 如何回收垃圾?

垃圾找到了,該怎么回收呢?看起來似乎是個很傻的問題。直接收起來扔掉不就好了?!對應到程序的操作,就是直接將這些對象占用的空間標記為空閑不就好了嗎?那我們就來看一下這個基礎的回收算法:標記-清除(Mark-Sweep)算法。

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

該算法很簡單,使用通過可達性分析分析方法標記出垃圾,然后直接回收掉垃圾區(qū)域。它的一個顯著問題是一段時間后,內(nèi)存會出現(xiàn)大量碎片,導致雖然碎片總和很大,但無法滿足一個大對象的內(nèi)存申請,從而導致 OOM,而過多的內(nèi)存碎片(需要類似鏈表的數(shù)據(jù)結(jié)構(gòu)維護),也會導致標記和清除的操作成本高,效率低下,如下圖所示:

image.png

1.2.2 復制算法(Copying)

為了解決上面算法的效率問題,有人提出了復制算法。它將可用內(nèi)存一分為二,每次只用一塊,當這一塊內(nèi)存不夠用時,便觸發(fā) GC,將當前存活對象復制(Copy)到另一塊上,以此往復。這種算法高效的原因在于分配內(nèi)存時只需要將指針后移,不需要維護鏈表等。但它最大的問題是對內(nèi)存的浪費,使用率只有 50%。

但這種算法在一種情況下會很高效:Java 對象的存活時間極短。據(jù) IBM 研究,Java 對象高達 98% 是朝生夕死的,這也意味著每次 GC 可以回收大部分的內(nèi)存,需要復制的數(shù)據(jù)量也很小,這樣它的執(zhí)行效率就會很高。

image.png

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

該算法解決了第1中算法的內(nèi)存碎片問題,它會在回收階段將所有內(nèi)存做整理,如下圖所示:

image.png

但它的問題也在于增加了整理階段,也就增加了 GC 的時間。1.2.4 分代收集算法(Generation Collection)

既然大部分 Java 對象是朝生夕死的,那么我們將內(nèi)存按照 Java 生存時間分為 新生代(Young)老年代(Old),前者存放短命僧,后者存放長壽佛,當然長壽佛也是由短命僧升級上來的。然后針對兩者可以采用不同的回收算法,比如對于新生代采用復制算法會比較高效,而對老年代可以采用標記-清除或者標記-整理算法。這種算法也是最常用的。JVM Heap 分代后的劃分一般如下所示,新生代一般會分為 Eden、Survivor0、Survivor1區(qū),便于使用復制算法。

image.png

將內(nèi)存分代后的 GC 過程一般類似下圖所示:

image.png
  1. 對象一般都是先在 Eden區(qū)創(chuàng)建
  2. Eden區(qū)滿,觸發(fā) Young GC,此時將 Eden中還存活的對象復制到 S0中,并清空 Eden區(qū)后繼續(xù)為新的對象分配內(nèi)存
  3. Eden區(qū)再次滿后,觸發(fā)又一次的 Young GC,此時會將 EdenS0中存活的對象復制到 S1中,然后清空EdenS0后繼續(xù)為新的對象分配內(nèi)存
  4. 每經(jīng)過一次 Young GC,存活下來的對象都會將自己存活次數(shù)加1,當達到一定次數(shù)后,會隨著一次 Young GC 晉升到 Old區(qū)
  5. Old區(qū)也會在合適的時機進行自己的 GC

1.2.5 常見的垃圾收集器

前面我們講了眾多的垃圾收集算法,那么其具體的實現(xiàn)就是垃圾收集器,也是我們實際使用中會具體用到的。現(xiàn)代的垃圾收集機制基本都是分代收集算法,而 YoungOld區(qū)分別有不同的垃圾收集器,簡單總結(jié)如下圖:

image.png

從上圖我們可以看到 YoungOld區(qū)有不同的垃圾收集器,實際使用時會搭配使用,也就是上圖中兩兩連線的收集器是可以搭配使用的。這些垃圾收集器按照運行原理大概可以分為如下幾類:

  • Serial GC,串行,單線程的收集器,運行 GC 時需要停止所有的用戶線程,且只有一個 GC 線程
  • Parallel GC并行,多線程的收集器,是 Serial 的多線程版,運行時也需要停止所有用戶線程,但同時運行多個 GC 線程,所以效率高一些
  • Concurrent GC,并發(fā),多線程收集器,GC 分多階段執(zhí)行,部分階段允許用戶線程與 GC 線程同時運行,這也就是并發(fā)的意思,大家要和并行做一個區(qū)分。
  • 其他

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

1.2.5.3 Concurrent Mark-Sweep GC

該類目前只是針對 Old 區(qū),最常見就是CMS GC,它的執(zhí)行分為多個階段,只有部分階段需要停止用戶進程,這里不詳細介紹了,感興趣可以去找相關文章來看,大體執(zhí)行如下:

image.png

1.2.5.4 其他

目前最新的 GC 有G1GCZGC,其運行機制與上述均不相同,雖然他們也是分代收集算法,但會把 Heap 分成多個 region 來做處理,這里不展開講,感興趣的可以參看最后參考資料的內(nèi)容。

image.png

1.2.6 Elasticsearch 的 GC 組合

Elasticsearch 默認的 GC 配置是CMS GC ,其 Young 區(qū)ParNew,Old 區(qū)CMS,大家可以在 config/jvm.options中看到如下的配置:

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

1.3 何時進行回收?

現(xiàn)在我們已經(jīng)知道如何找到和回收垃圾了,那么什么時候回收呢?簡單總結(jié)如下:

  1. Young 區(qū)的GC 都是在 Eden 區(qū)滿時觸發(fā)
  2. Serial Old 和 Parallel Old 在 Old 區(qū)是在 Young GC 時預測Old 區(qū)是否可以為 young 區(qū) promote 到 old 區(qū) 的 object 分配空間,如果不可用則觸發(fā) Old GC。這個也可以理解為是 Old區(qū)滿時。
  3. CMS GC 是在 Old 區(qū)大小超過一定比例后觸發(fā),而不是 Old 區(qū)滿。這個原因在于 CMS GC 是并發(fā)的算法,也就是說在 GC 線程收集垃圾的時候,用戶線程也在運行,因此需要預留一些 Heap 空間給用戶線程使用,防止由于無法分配空間而導致 Full GC 發(fā)生。

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 內(nèi)花了 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 啟動至今發(fā)生的第 160772次 GC]
  • duration [本次檢查到的 GC 總耗時 5 秒,可能是多次的加和],
  • collections [從上次檢查至今總共發(fā)生1次 GC]/[從上次檢查至今已過去 5.1 秒],
  • total [本次檢查到的 GC 總耗時為 5 秒]/[從 JVM 啟動至今發(fā)生的 GC 總耗時為 4.4 天],
  • memory [ GC 前 Heap memory 空間]->[GC 后 Heap memory 空間]/[Heap memory 總空間],
  • all_pools(分代部分的詳情) {[young 區(qū)][GC 前 Memory ]->[GC后 Memory]/[young區(qū) Memory 總大小] } {[survivor 區(qū)][GC 前 Memory ]->[GC后 Memory]/[survivor區(qū) Memory 總大小] }{[old 區(qū)][GC 前 Memory ]->[GC后 Memory]/[old區(qū) Memory 總大小] }

3. 看看源碼

從日志中我們可以看到輸出這些日志的類名叫做JvmGcMonitorService,我們?nèi)ピ创a中搜索很快會找到它/Users/rockybean/code/elasticsearch/core/src/main/java/org/elasticsearch/monitor/jvm/JvmGcMonitorService.java,這里就不詳細展開講解源碼了,它執(zhí)行的內(nèi)容大概如下圖所示:

image.png

關于打印日志的格式在源碼也有,如下所示:

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 [{}]";

另外細心的同學會發(fā)現(xiàn)輸出的日志中 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;
    }

在上面的代碼中,你會看到很多我們在上一節(jié)中提到的 GC 算法的名稱。

至此,源碼相關部分也講解完畢,感興趣的大家可以自行去查閱。

4. 總結(jié)

講解 GC 的文章已經(jīng)很多,本文又嘮嘮叨叨地講一遍基礎知識,是希望對于第一次了解 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高級特性與最佳實踐》
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 如果1你關注過 elasticsearch 的日志,可能會看到如下類似的內(nèi)容: 看到其中的[gc]關鍵詞你也猜到了...
    rockybean閱讀 10,685評論 2 24
  • 原文閱讀 前言 這段時間懈怠了,罪過! 最近看到有同事也開始用上了微信公眾號寫博客了,挺好的~給他們點贊,這博客我...
    碼農(nóng)戲碼閱讀 6,163評論 2 31
  • 作者:一字馬胡 轉(zhuǎn)載標志 【2017-11-12】 更新日志 日期更新內(nèi)容備注 2017-11-12新建文章初版 ...
    beneke閱讀 2,331評論 0 7
  • 轉(zhuǎn)載blog.csdn.net/ning109314/article/details/10411495/ JVM工...
    forever_smile閱讀 5,516評論 1 56
  • 內(nèi)存溢出和內(nèi)存泄漏的區(qū)別 內(nèi)存溢出:out of memory,是指程序在申請內(nèi)存時,沒有足夠的內(nèi)存空間供其使用,...
    Aimerwhy閱讀 808評論 0 1

友情鏈接更多精彩內(nèi)容