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個原因著手分析:
- 內(nèi)存泄漏
- 長時間的 gc pause
- 連續(xù)的 full GC
- 等待 I/O 、CPU 等
處理方案
內(nèi)存泄漏
首先我們看一下 most notorious 的第一個原因,Memory Leak。主要現(xiàn)象,是 log 中出現(xiàn)的 OutOfMemory 異常。這類 OOM 錯誤一般有8種。其中,5類在 JVM heap上:
- Java heap space
- GC overhead limit exceeded
- Requested array size exceeds VM limit
- Permgen space
- Metaspace
另外3類是:
- Unable to create new native thread
- Kill process or sacrifice child.
- reason stack_trace_with_native_method
分析這8類問題的形成機制,已經(jīng)有珠玉在前,這里不再贅述。這類問題的具體分析,依賴 heap dump工具,MAT(Eclipse Memory Analyzer) 就是一個比較理想的工具。
Long GC pause
導(dǎo)致long gc pause 的主要原因有:
- 創(chuàng)建對象太多太快
- young gen 的 size 太小
- GC 算法選擇不當
- 系統(tǒng)發(fā)生了 swapping
- GC thread 不夠
- 后臺 IO 太多
- 顯式調(diào)用了 System.gc(),這會導(dǎo)致 STW(stop the world)
- Heap size 設(shè)置的過大
接下來,我們依次講解一下這些問題的處理方式。
創(chuàng)建對象太快,可以通過 Heap dump 來發(fā)現(xiàn),解決方案需要具體分析,通常是優(yōu)化程序,或者增加 app worker 個數(shù)。
Young gen 的 size 太小,可以通過修改以下兩個參數(shù)來調(diào)整:
- -Xmn:直接調(diào)整 Young generation 的大小
- -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)用的來源可能有以下幾種:
- 顯式調(diào)用
- 第三方庫
- RMI
- 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 的原因,有以下幾類:
并發(fā)模式失敗
G1啟動了標記周期,但在Mix GC之前,老年代就被填滿,這時候G1會放棄標記周期。-
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)鍵部分摘錄,供大家參考。- 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.
- 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
- 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.
- 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.
- 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.
巨型對象分配失敗
這個問題我們并沒有遇到過,但是如果你遇到了,日志里會有以下字樣:
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 空間。處理方法:
- 增加 Heap size
- 增加 perm gen/metaspace size。
- 更多的機器!
除了上述的常規(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ù),讓它自己處理。如果你實在忍不住要動,可以參考以下文章:
最后,GC 的分析,有一個很好用的在線工具,gceasy.io,上傳 GC log 文件就可以進行在線智能分析。本文也大量參考了 gceasy 的文檔。