Java App GC 性能分析和異常處理

JVM 發(fā)展到今天,已經(jīng)相當成熟。如果我們使用 G1作為垃圾回收方案,則配置上更是輕松很多,除了暫停時間和 xms、xmx,其他幾乎都不用管。

當然,這是理想情況。實際工作中,無論是開源工具還是企業(yè)自己開發(fā)的 Application,都會出現(xiàn)性能問題,這些問題,又大多和 GC 有關(guān)。本文總結(jié)一下常見的一些 GC 異常場景和它們的處理方法。

目的

GC 優(yōu)化的目的,是讓程序達到盡可能高的 throughput(吞吐量):即 JVM 在處理業(yè)務(wù)的時間和在維護自身的時間(主要是 gc)的比例。 通常這個數(shù)字應(yīng)該在95%以上。

如果你發(fā)現(xiàn) Java 程序的吞吐量低,可以從以下4個原因著手分析:

  1. 內(nèi)存泄漏
  2. 長時間的 gc pause
  3. 連續(xù)的 full GC
  4. 等待 I/O 、CPU 等

處理方案

內(nèi)存泄漏

首先我們看一下 most notorious 的第一個原因,Memory Leak。主要現(xiàn)象,是 log 中出現(xiàn)的 OutOfMemory 異常。這類 OOM 錯誤一般有8種。其中,5類在 JVM heap上:

  1. Java heap space
  2. GC overhead limit exceeded
  3. Requested array size exceeds VM limit
  4. Permgen space
  5. Metaspace

另外3類是:

  1. Unable to create new native thread
  2. Kill process or sacrifice child.
  3. reason stack_trace_with_native_method

分析這8類問題的形成機制,已經(jīng)有珠玉在前,這里不再贅述。這類問題的具體分析,依賴 heap dump工具,MAT(Eclipse Memory Analyzer) 就是一個比較理想的工具。

Long GC pause

導(dǎo)致long gc pause 的主要原因有:

  1. 創(chuàng)建對象太多太快
  2. young gen 的 size 太小
  3. GC 算法選擇不當
  4. 系統(tǒng)發(fā)生了 swapping
  5. GC thread 不夠
  6. 后臺 IO 太多
  7. 顯式調(diào)用了 System.gc(),這會導(dǎo)致 STW(stop the world)
  8. Heap size 設(shè)置的過大

接下來,我們依次講解一下這些問題的處理方式。

創(chuàng)建對象太快,可以通過 Heap dump 來發(fā)現(xiàn),解決方案需要具體分析,通常是優(yōu)化程序,或者增加 app worker 個數(shù)。

Young gen 的 size 太小,可以通過修改以下兩個參數(shù)來調(diào)整:

  1. -Xmn:直接調(diào)整 Young generation 的大小
  2. -XX:NewRatio:這個參數(shù)調(diào)整 old/young 的比例。如果你用 G1,不要設(shè)置

GC 算法選擇的問題比較復(fù)雜。總的來說建議在大內(nèi)存的服務(wù)器(超過48g)、JDK 8以上的環(huán)境,使用G1。G1只需要設(shè)置這個參數(shù):-XX:MaxGCPauseMillis。

Swap 的問題是一個很常見的問題。由于需要的頁被換出內(nèi)存,導(dǎo)致未命中的情況,會產(chǎn)生大量的磁盤 IO,嚴重降低 GC 速度。因此在吞吐量要求高的系統(tǒng)上,需要禁用 swap

dstat工具、或者更常見的free -h命令都能夠用來檢測 swapping。如果你想知道具體哪些進程發(fā)生了 swapping,可以用下面這個腳本:

#!/bin/bash
# Get current swap usage for all running processes
# Erik Ljungstrom 27/05/2011
# Modified by Mikko Rantalainen 2012-08-09
# Pipe the output to "sort -nk3" to get sorted output
# Modified by Marc Methot 2014-09-18
# removed the need for sudo

SUM=0
OVERALL=0
for DIR in `find /proc/ -maxdepth 1 -type d -regex "^/proc/[0-9]+"`
do
    PID=`echo $DIR | cut -d / -f 3`
    PROGNAME=`ps -p $PID -o comm --no-headers`
    for SWAP in `grep VmSwap $DIR/status 2>/dev/null | awk '{ print $2 }'`
    do
        let SUM=$SUM+$SWAP
    done
    if (( $SUM > 0 )); then
        echo "PID=$PID swapped $SUM KB ($PROGNAME)"
    fi
    let OVERALL=$OVERALL+$SUM
    SUM=0
done
echo "Overall swap used: $OVERALL KB"

一般來說,對于一個專用的 Java server,關(guān)閉 swapping 是最理想的選擇。如果實在無法做到,可以通過增加內(nèi)存、降低 heap size、關(guān)閉其他不相干進程等方式,緩解 swapping 的問題。

GC thread 不夠的問題,不太常見。因為通常服務(wù)器的 CPU 都是多核的,分配多核給 GC thread 也是很普遍的行為。GC thread 不夠,可以從 GC log 中看出來,例如:

[Times: user=25.56 sys=0.35, real=20.48 secs] 

這里,real 時間,是 wall clock,即真實消耗的時間。user/sys,分別表示在用戶態(tài)和核心態(tài)消耗的時間。如果有多個線程同時工作,時間會累加起來。如果我們有5個 GC thread,那么 user 應(yīng)該差不多等于 real 的 5倍。如果 real 時間比較大,而 user 比 real 大的倍數(shù)不多,那么我們就需要更多的 GC thread 了。

后臺 IO 過多的現(xiàn)象同樣可以通過 dstat 等工具發(fā)現(xiàn),還有個比較實用的技巧就是和上面的問題一樣,看 GC Times。如果 real 比 user 多,那么毫無疑問很多時間被用在了 IO 上。現(xiàn)在的 server 一般多采用異步模式,這個問題出現(xiàn)的概率應(yīng)該不高。

System.gc() 的調(diào)用,也會導(dǎo)致 STW。該調(diào)用的來源可能有以下幾種:

  1. 顯式調(diào)用
  2. 第三方庫
  3. RMI
  4. JMX

可以通過 -XX:+DisableExplicitGC 來阻止程序顯式調(diào)用 GC。

最后,如果 Heap Size 過大也會影響 GC 速度。但是這點我不是很確定。理論上大的 Heap 會降低 GC 的頻率,影響到底有多大,需要具體分析。

連續(xù) Full GC

Full GC 占用的系統(tǒng)資源很高。它會清理 Heap 中所有的 gen(Young, Old, Perm, Metaspace)。在 Full GC 中很多步驟是 STW 的,如 initial-mark,remark, cleanup。在這個過程中,業(yè)務(wù)代碼停止運行,JVM 用全部的 CPU 來執(zhí)行 GC,同時也會導(dǎo)致 CPU 占用率飆升。 總的來說,F(xiàn)ull GC 應(yīng)當避免,連續(xù) Full GC 更應(yīng)該避免。

連續(xù) Full GC 的原因,有以下幾類:

  1. 并發(fā)模式失敗
    G1啟動了標記周期,但在Mix GC之前,老年代就被填滿,這時候G1會放棄標記周期。

  2. promotion 失敗或者 evacuation 失敗
    在進行 GC 的時候,沒有足夠的內(nèi)存供存活對象或晉升對象使用,由此觸發(fā)了Full GC。日志中通常會出現(xiàn)以下字樣:"evacuation failure", "to-space exhausted", "to-space overflow"。如果你是從 CMS 之類的 GC 切換到 G1,記得把分配 Heap 比例的幾個選項關(guān)閉。另外,有幾個選項對這個現(xiàn)象有一定影響,如 -XX:InitiatingHeapOccupancyPercent 和 -XX:G1ReservePercent。如果你試圖改一改這個,確保你先看過這篇文章。這里我把原文關(guān)鍵部分摘錄,供大家參考。

    1. Find out if the failures are a side effect of over-tuning - Get a simple baseline with min and max heap and a realistic pause time goal: Remove any additional heap sizing such as -Xmn, -XX:NewSize, -XX:MaxNewSize, -XX:SurvivorRatio, etc. Use only -Xms, -Xmx and a pause time goal -XX:MaxGCPauseMillis.
    1. If the problem persists even with the baseline run and if humongous allocations (see next section below) are not the issue - the corrective action is to increase your Java heap size, if you can, of course
    1. If increasing the heap size is not an option and if you notice that the marking cycle is not starting early enough for G1 GC to be able to reclaim the old generation then drop your -XX:InitiatingHeapOccupancyPercent. The default for this is 45% of your total Java heap. Dropping the value will help start the marking cycle earlier. Conversely, if the marking cycle is starting early and not reclaiming much, you should increase the threshold above the default value to make sure that you are accommodating for the live data set for your application.
    1. If concurrent marking cycles are starting on time, but are taking a lot of time to finish; and hence are delaying the mixed garbage collection cycles which will eventually lead to an evacuation failure since old generation is not timely reclaimed; increase the number of concurrent marking threads using the command line option: -XX:ConcGCThreads.
    1. If "to-space" survivor is the issue, then increase the -XX:G1ReservePercent. The default is 10% of the Java heap. G1 GC creates a false ceiling and reserves the memory, in case there is a need for more "to-space". Of course, G1 GC caps it off at 50%, since we do not want the end-user to set it to a very large value.
  3. 巨型對象分配失敗
    這個問題我們并沒有遇到過,但是如果你遇到了,日志里會有以下字樣:

1361.680: [G1Ergonomics (Concurrent Cycles) request concurrent cycle initiation, reason: occupancy higher than threshold, occupancy: 1459617792 bytes, allocation request: 4194320 bytes, threshold: 1449551430 bytes (45.00 %), source: concurrent humongous allocation]

可以通過增加 Heap size 或者增大 -XX:G1HeapRegionSize 來解決。

總之,F(xiàn)ull GC 根源還是 JVM heap 大小分配的不夠,意味著 JVM 需要更多 heap 空間。處理方法:

  1. 增加 Heap size
  2. 增加 perm gen/metaspace size。
  3. 更多的機器!

除了上述的常規(guī)處理方案,我們也需要考慮程序 bug 的情況。舉例來說,我們的 Cassandra 集群就經(jīng)常遇到以下的連續(xù) Full GC:

2018-11-30T16:28:57.196+0800: 269569.472: [Full GC (Allocation Failure)  23G->22G(24G), 80.8007295 secs]
2018-11-30T16:30:22.152+0800: 269654.428: [Full GC (Allocation Failure)  23G->22G(24G), 83.1106023 secs]
2018-11-30T16:31:48.893+0800: 269741.169: [Full GC (Allocation Failure)  23G->22G(24G), 80.5397583 secs]
2018-11-30T16:33:13.391+0800: 269825.666: [Full GC (Allocation Failure)  23G->22G(24G), 83.3309248 secs]
2018-11-30T16:34:40.599+0800: 269912.874: [Full GC (Allocation Failure)  23G->22G(24G), 80.2643310 secs]

雖然官方?jīng)]有確認,但我覺得這是一個 bug:https://issues.apache.org/jira/browse/CASSANDRA-13365

總結(jié)

G1 的優(yōu)化,就是盡量不要設(shè)置多余的參數(shù),讓它自己處理。如果你實在忍不住要動,可以參考以下文章:

  1. Tips for Tuning the Garbage First Garbage Collector
  2. Garbage First Garbage Collector Tuning

最后,GC 的分析,有一個很好用的在線工具,gceasy.io,上傳 GC log 文件就可以進行在線智能分析。本文也大量參考了 gceasy 的文檔。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內(nèi)容