若是你關注過 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 相關的日誌,那麼你瞭解每一部分的含義嗎?若是不瞭解,你能夠繼續往下看了。算法
咱們先從最簡單的看起:編程
日誌發生的時間
日誌級別
,這裏分別是WARN
和INFO
輸出日誌的類
,咱們後面也會講到這個類當前 ES 節點名稱
gc
關鍵詞,咱們就從這個關鍵詞聊起。友情提示:
對 GC 已經瞭如指掌的同窗,能夠直接翻到最後看答案。數據結構
GC,全稱是 Garbage Collection
(垃圾收集)或者 Garbage Collector
(垃圾收集器)。多線程
在使用 C語言編程的時候,咱們要手動的經過 malloc
和 free
來申請和釋放數據須要的內存,若是忘記釋放內存,就會發生內存泄露的狀況,即無用的數據佔用了寶貴的內存資源。而Java 語言編程不須要顯示的申請和釋放內存,由於 JVM 能夠自動管理內存,這其中最重要的一部分就是 GC
,即 JVM 能夠自主地去釋放無用數據(垃圾)佔用的內存。併發
咱們研究 GC 的主要緣由是 GC 的過程會有 Stop The World
(STW)的狀況發生,即此時用戶線程會中止工做,若是 STW 的時間過長,則應用的可用性、實時性等就降低的很厲害。jvm
GC
主要解決以下3個問題:elasticsearch
咱們一個個來看下。spa
所謂垃圾,指的是再也不被使用(引用)的對象。Java 的對象都是在堆(Heap)上建立的,咱們這裏默認也只討論堆。那麼如今問題就變爲如何斷定一個對象是否還有被引用,思路主要有以下兩種:線程
方法1簡單粗暴效率高,但準確度不行,尤爲是面對互相引用的垃圾對象時無能爲力。
方法2是目前經常使用的方法,這裏有一個關鍵是 GC Roots
,它是斷定的源頭,感興趣的同窗能夠本身去研究下,這裏就不展開講了。
垃圾找到了,該怎麼回收呢?看起來彷佛是個很傻的問題。直接收起來扔掉不就行了?!對應到程序的操做,就是直接將這些對象佔用的空間標記爲空閒不就行了嗎?那咱們就來看一下這個基礎的回收算法:標記-清除(Mark-Sweep)算法。
該算法很簡單,使用經過可達性分析分析方法標記出垃圾,而後直接回收掉垃圾區域。它的一個顯著問題是一段時間後,內存會出現大量碎片,致使雖然碎片總和很大,但沒法知足一個大對象的內存申請,從而致使 OOM,而過多的內存碎片(須要相似鏈表的數據結構維護),也會致使標記和清除的操做成本高,效率低下,以下圖所示:
爲了解決上面算法的效率問題,有人提出了複製算法。它將可用內存一分爲二,每次只用一塊,當這一塊內存不夠用時,便觸發 GC,將當前存活對象複製(Copy)到另外一塊上,以此往復。這種算法高效的緣由在於分配內存時只須要將指針後移,不須要維護鏈表等。但它最大的問題是對內存的浪費,使用率只有 50%。
但這種算法在一種狀況下會很高效:Java 對象的存活時間極短。據 IBM 研究,Java 對象高達 98% 是朝生夕死的,這也意味着每次 GC 能夠回收大部分的內存,須要複製的數據量也很小,這樣它的執行效率就會很高。
該算法解決了第1中算法的內存碎片問題,它會在回收階段將全部內存作整理,以下圖所示:
但它的問題也在於增長了整理階段,也就增長了 GC 的時間。
既然大部分 Java 對象是朝生夕死的,那麼咱們將內存按照 Java 生存時間分爲 新生代(Young)
和 老年代(Old)
,前者存放短命僧,後者存放長壽佛,固然長壽佛也是由短命僧升級上來的。而後針對二者能夠採用不一樣的回收算法,好比對於新生代
採用複製算法會比較高效,而對老年代
能夠採用標記-清除或者標記-整理算法。這種算法也是最經常使用的。JVM Heap 分代後的劃分通常以下所示,新生代通常會分爲 Eden、Survivor0、Survivor1區,便於使用複製算法。
將內存分代後的 GC 過程通常相似下圖所示:
Eden
區建立Eden
區滿,觸發 Young GC,此時將 Eden
中還存活的對象複製到 S0
中,並清空 Eden
區後繼續爲新的對象分配內存Eden
區再次滿後,觸發又一次的 Young GC,此時會將 Eden
和S0
中存活的對象複製到 S1
中,而後清空Eden
和S0
後繼續爲新的對象分配內存Old
區Old
區也會在合適的時機進行本身的 GC前面咱們講了衆多的垃圾收集算法,那麼其具體的實現就是垃圾收集器,也是咱們實際使用中會具體用到的。現代的垃圾收集機制基本都是分代收集算法,而 Young
與 Old
區分別有不一樣的垃圾收集器,簡單總結以下圖:
從上圖咱們能夠看到 Young
與 Old
區有不一樣的垃圾收集器,實際使用時會搭配使用,也就是上圖中兩兩連線的收集器是能夠搭配使用的。這些垃圾收集器按照運行原理大概能夠分爲以下幾類:
咱們下面簡單看一下他們的運行機制。
該類 Young區
的爲 Serial GC
,Old區
的爲Serial Old GC
。執行大體以下所示:
該類Young 區
的有 ParNew
和 Parallel Scavenge
,Old 區
的有Parallel Old
。其運行機制以下,相比 Serial GC ,其最大特色在於 GC 線程是並行的,效率高不少:
該類目前只是針對 Old 區
,最多見就是CMS GC
,它的執行分爲多個階段,只有部分階段須要中止用戶進程,這裏不詳細介紹了,感興趣能夠去找相關文章來看,大致執行以下:
目前最新的 GC 有G1GC
和ZGC
,其運行機制與上述均不相同,雖然他們也是分代收集算法,但會把 Heap 分紅多個 region 來作處理,這裏不展開講,感興趣的能夠參看最後參考資料的內容。
Elasticsearch 默認的 GC 配置是CMS GC
,其 Young 區
用 ParNew
,Old 區
用CMS
,你們能夠在 config/jvm.options
中看到以下的配置:
## GC configuration -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly
如今咱們已經知道如何找到和回收垃圾了,那麼何時回收呢?簡單總結以下:
Young 區
的GC 都是在 Eden 區
滿時觸發Old 區
是在 Young GC 時預測Old 區是否能夠爲 young 區 promote 到 old 區 的 object 分配空間,若是不可用則觸發 Old GC。這個也能夠理解爲是 Old區
滿時。Old 區
大小超過必定比例後觸發,而不是 Old 區滿。這個緣由在於 CMS GC 是併發的算法,也就是說在 GC 線程收集垃圾的時候,用戶線程也在運行,所以須要預留一些 Heap 空間給用戶線程使用,防止因爲沒法分配空間而致使 Full GC 發生。前面講了這麼多,終於能夠回到開篇的問題了,咱們直接來看答案
[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 基礎知識,你們在看這裏解釋的時候就很是清楚了。
從日誌中咱們能夠看到輸出這些日誌的類名叫作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 算法的名稱。
至此,源碼相關部分也講解完畢,感興趣的你們能夠自行去查閱。
講解 GC 的文章已經不少,本文又嘮嘮叨叨地講一遍基礎知識,是但願對於第一次瞭解 GC 的同窗有所幫助。由於只有瞭解了這些基礎知識,你纔不至於被這些 GC 的輸出嚇懵。但願本文對你理解 ES 的 GC 日誌 有所幫助。
若是你想深刻的瞭解 JAVA GC 的知識,能夠關注 ElasticTalk
公衆號,回覆 GC
關鍵詞後便可獲取做者推薦的電子書等資料。