你看懂 Elasticsearch Log 中的 GC 日誌了嗎?
如果你關注過 elasticsearch 的日誌,可能會看到如下類似的內容:
[
2018
-
06
-
30
T17
:
57
:
23
,
848
][
WARN
][
o
。
e
。
m
。
j
。
JvmGcMonitorService
] [
qoo
——
eS
] [
gc
][
228384
]
overhead
,
spent
[
2。2
s
]
collecting
in
the
last
[
2。3
s
]
[
2018
-
06
-
30
T17
:
57
:
29
,
020
][
INFO
][
o
。
e
。
m
。
j
。
JvmGcMonitorService
] [
qoo
——
eS
] [
gc
][
old
][
228385
][
160772
]
duration
[
5
s
],
collections
[
1
]
/
[
5。1
s
],
total
[
5
s
]
/
[
4。4
d
],
memory
[
945。4
mb
]
->
[
958。5
mb
]
/
[
1007。3
mb
],
all_pools
{[
young
] [
87。8
mb
]
->
[
100。9
mb
]
/
[
133。1
mb
]}{[
survivor
] [
0
b
]
->
[
0
b
]
/
[
16。6
mb
]}{[
old
] [
857。6
mb
]
->
[
857。6
mb
]
/
[
857。6
mb
]}
看到其中的
[gc]
關鍵詞你也猜到了這是與 GC 相關的日誌,那麼你瞭解每一部分的含義嗎?如果不瞭解,你可以繼續往下看了。
我們先從最簡單的看起:
第一部分是
日誌發生的時間
第二部分是
日誌級別
,這裡分別是
WARN
和
INFO
第三部分是
輸出日誌的類
,我們後面也會講到這個類
第四部分是
當前 ES 節點名稱
第五部分是
gc
關鍵詞,我們就從這個關鍵詞聊起。
友情提示:
對 GC 已經瞭如指掌的同學,可以直接翻到最後看答案。
1。 什麼是 GC?
GC,全稱是
Garbage Collection
(垃圾收集)或者
Garbage Collector
(垃圾收集器)。
在使用 C語言程式設計的時候,我們要手動的透過
malloc
和
free
來申請和釋放資料需要的記憶體,如果忘記釋放記憶體,就會發生記憶體洩露的情況,即無用的資料佔用了寶貴的記憶體資源。而Java 語言程式設計不需要顯示的申請和釋放記憶體,因為 JVM 可以自動管理記憶體,這其中最重要的一部分就是
GC
,即 JVM 可以自主地去釋放無用資料(垃圾)佔用的記憶體。
我們研究 GC 的主要原因是 GC 的過程會有
Stop The World
(STW)的情況發生,即此時使用者執行緒會停止工作,如果 STW 的時間過長,則應用的可用性、實時性等就下降的很厲害。
主要解決如下3個問題:
如何找到垃圾?
如何回收垃圾?
何時回收垃圾?
我們一個個來看下。
1。1 如何找到垃圾?
所謂垃圾,指的是不再被使用(引用)的物件。Java 的物件都是在堆(Heap)上建立的,我們這裡預設也只討論堆。那麼現在問題就變為如何判定一個物件是否還有被引用,思路主要有如下兩種:
引用計數法
,即在物件被引用時加1,去除引用時減1,如果引用值為0,即表明該物件可回收了。
可達性分析法
,即透過遍歷已知的存活物件(
GC Roots
)的引用鏈來標記出所有存活物件
方法2是目前常用的方法,這裡有一個關鍵是
GC Roots
,它是判定的源頭,感興趣的同學可以自己去研究下,這裡就不展開講了。
image。png
1。2 如何回收垃圾?
垃圾找到了,該怎麼回收呢?看起來似乎是個很傻的問題。直接收起來扔掉不就好了?!對應到程式的操作,就是直接將這些物件佔用的空間標記為
空閒
不就好了嗎?那我們就來看一下這個基礎的回收演算法:標記-清除(Mark-Sweep)演算法。
1。2。1 標記-清除 演算法(Mark Sweep)
該演算法很簡單,使用透過
可達性分析
分析方法標記出垃圾,然後直接回收掉垃圾區域。它的一個顯著問題是一段時間後,記憶體會出現
大量碎片
,導致雖然碎片總和很大,但無法滿足一個大物件的記憶體申請,從而導致 OOM,而過多的記憶體碎片(需要類似連結串列的資料結構維護),也會導致標記和清除的操作成本高,效率低下,如下圖所示:
image。png
1。2。2 複製演算法(Copying)
為了解決上面演算法的效率問題,有人提出了複製演算法。它將可用
記憶體一分為二
,每次只用一塊,當這一塊記憶體不夠用時,便觸發 GC,將當前存活物件複製(Copy)到另一塊上,以此往復。這種演算法高效的原因在於分配記憶體時只需要將指標後移,不需要維護連結串列等。但它
最大的問題是對記憶體的浪費
,使用率只有 50%。
但這種演算法在一種情況下會很高效:Java 物件的存活時間極短。據 IBM 研究,Java 物件高達 98% 是
朝生夕死
的,這也意味著每次 GC 可以回收大部分的記憶體,需要複製的資料量也很小,這樣它的執行效率就會很高。
image。png
1。2。3 標記-整理演算法(Mark Compact)
該演算法解決了第1中演算法的記憶體碎片問題,它會在回收階段將所有記憶體做整理,如下圖所示:
image。png
但它的問題也在於增加了整理階段,也就增加了 GC 的時間。1。2。4 分代收集演算法(Generation Collection)
既然大部分 Java 物件是
朝生夕死
的,那麼我們將記憶體按照 Java 生存時間分為
新生代(Young)
和
老年代(Old)
,前者存放短命僧,後者存放長壽佛,當然長壽佛也是由短命僧升級上來的。然後針對兩者可以採用不同的回收演算法,比如對於
新生代
採用複製演算法會比較高效,而對
老年代
可以採用標記-清除或者標記-整理演算法。這種演算法也是最常用的。JVM Heap 分代後的劃分一般如下所示,新生代一般會分為 Eden、Survivor0、Survivor1區,便於使用複製演算法。
image。png
將記憶體分代後的 GC 過程一般類似下圖所示:
image。png
物件一般都是先在
Eden
區建立
當
區滿,觸發 Young GC,此時將
中還存活的物件複製到
S0
中,並清空
區後繼續為新的物件分配記憶體
當
區再次滿後,觸發又一次的 Young GC,此時會將
和
中存活的物件複製到
S1
中,然後清空
和
後繼續為新的物件分配記憶體
每經過一次 Young GC,存活下來的物件都會將自己存活次數加1,當達到一定次數後,會隨著一次 Young GC 晉升到
Old
區
區也會在合適的時機進行自己的 GC
1。2。5 常見的垃圾收集器
前面我們講了眾多的垃圾收集演算法,那麼其具體的實現就是垃圾收集器,也是我們實際使用中會具體用到的。現代的垃圾收集機制基本都是分代收集演算法,而
Young
與
區分別有不同的垃圾收集器,簡單總結如下圖:
image。png
從上圖我們可以看到
與
區有不同的垃圾收集器,實際使用時會搭配使用,也就是上圖中兩兩連線的收集器是可以搭配使用的。這些垃圾收集器按照執行原理大概可以分為如下幾類:
Serial GC
,
序列
,單執行緒的收集器,執行 GC 時需要停止所有的使用者執行緒,且只有一個 GC 執行緒
Parallel GC
,
並行
,多執行緒的收集器,是 Serial 的多執行緒版,執行時也需要停止所有使用者執行緒,但同時執行多個 GC 執行緒,所以效率高一些
Concurrent GC
,
併發
,多執行緒收集器,GC 分多階段執行,部分階段允許使用者執行緒與 GC 執行緒同時執行,這也就是併發的意思,大家要和並行做一個區分。
其他
我們下面簡單看一下他們的執行機制。
1。2。5。3 Concurrent Mark-Sweep GC
該類目前只是針對
Old 區
,最常見就是
CMS GC
,它的執行分為多個階段,只有部分階段需要停止使用者程序,這裡不詳細介紹了,感興趣可以去找相關文章來看,大體執行如下:
image。png
1。2。5。4 其他
目前最新的 GC 有
G1GC
和
ZGC
,其執行機制與上述均不相同,雖然他們也是分代收集演算法,但會把 Heap 分成多個 region 來做處理,這裡不展開講,感興趣的可以參看最後參考資料的內容。
image。png
1。2。6 Elasticsearch 的 GC 組合
Elasticsearch 預設的 GC 配置是
,其
Young 區
用
ParNew
,
用
CMS
,大家可以在
config/jvm。options
中看到如下的配置:
## GC configuration
-
XX
:
+
UseConcMarkSweepGC
-
XX
:
CMSInitiatingOccupancyFraction
=
75
-
XX
:
+
UseCMSInitiatingOccupancyOnly
1。3 何時進行回收?
現在我們已經知道如何找到和回收垃圾了,那麼什麼時候回收呢?簡單總結如下:
的GC 都是在
Eden 區
滿時觸發
Serial Old 和 Parallel Old 在
是在 Young GC 時預測Old 區是否可以為 young 區 promote 到 old 區 的 object 分配空間,如果不可用則觸發 Old GC。這個也可以理解為是
滿時。
CMS GC 是在
大小超過一定比例後觸發,而不是 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
在最近
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
這是第
228385
次 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]/[young區 Memory 總大小] } {
survivor
區
->[GC後 Memory]/[survivor區 Memory 總大小] }{
old
區
->[GC後 Memory]/[old區 Memory 總大小] }
3。 看看原始碼
從日誌中我們可以看到輸出這些日誌的類名叫做
JvmGcMonitorService
,我們去原始碼中搜索很快會找到它
/Users/rockybean/code/elasticsearch/core/src/main/java/org/elasticsearch/monitor/jvm/JvmGcMonitorService。java
,這裡就不詳細展開講解原始碼了,它執行的內容大概如下圖所示:
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 [{}]”
;
另外細心的同學會發現輸出的日誌中 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 日誌 有所幫助。