1. 程式人生 > >京東某系統雙十一記憶體飆升分析和解決方案

京東某系統雙十一記憶體飆升分析和解決方案

一、問題現象

系統在雙十一期間出現頻繁記憶體飆升現象,記憶體在幾天內直接飆升到報警閾值。在堆記憶體空間由3g調整到4g後,依然出現堆記憶體超過閾值問題,同時在重啟若干次記憶體依然會飆升堆積。同時也發現了jvm一直不出現full gc,young gc稍微有點顫抖並出現young gc回收慢現象,很詭異,where is the full gc?!如圖所示:

這裡寫圖片描述

同時可以看到,young gc後,記憶體逐漸飆升,時快時慢,可以看到有時young gc回收不了多少空間,如圖:

這裡寫圖片描述

而cpu使用率也隨著young gc事件的產生出現抖動,很不平穩,如圖:

這裡寫圖片描述

流量大時,最恐怖的是這樣:

這裡寫圖片描述

這裡寫圖片描述

於是懷疑有記憶體洩露問題,dump了3g多的線上記憶體映象,進行分析。同時也找了架構,給出的意見是更改觸發full gc條件,降低tomcat執行緒數。

二、分析過程

懷疑記憶體洩露,於是使用工具分析dump檔案,but….
分析使用工具:

  1. MAT 點選下載 例項講解1 例項講解2
  2. BHeapSampler 點選下載

分析過程中遇到了一個問題,由於MAT本身是eclipse的分支,眾所周知eclipse是個java寫的編譯器,限制於jvm堆記憶體。3g多的dump檔案匯入MAT會直接把MAT的記憶體撐滿,當時分析的時候忘記了可以直接改MAT的配置檔案提升堆記憶體。於是找了個神奇的命令列工具-BHeapSampler,當然這貨也是java寫的,分析速度很快,生成報告也很直接,很給力!
當然後來就想起了MAT能夠調整堆記憶體,這裡先說明一下使用BHeapSampler,話說這東西在網上的文件挺少的,只能看著官方文件摸索學習。
執行命令java -jar -Xmx4g -Xms4g bheapsampler.jar ~/Desktop/jmap-20161114094315.bin
解釋:~/Desktop/jmap-20161114094315.bin是我匯出的dump檔案, -Xmx4g和-Xms4g同樣需要配置堆記憶體,3g多的dump檔案給了4g做分析。
BHeapSampler會在bheapsampler.jar的目錄下生成名為memory_paths.txt的分析報告檔案,是記憶體物件的引用鏈。

這裡寫圖片描述

這裡寫圖片描述

可以看到有大量的io/netty/util/RecyclerWeakOrderQueueLink和com/mysql/jdbc/JDBC4Connection的java/lang/ref/Finalizer物件。
已知netty的Recycler中的WeakOrderQueue用於netty對大量連線產生的物件回收時進行快取池化操作,下次再用到該物件時從池中取出,減少了過量建立物件和銷燬物件產生的壓力。
已知mysql的Connection/J中呼叫了finalize(),在finalize()方法中會呼叫cleanup()方法,該方法用於關閉connection連線。
查閱netty相關資料,在netty的github上找到了一個issue

#4147,大致可以看出,netty實現的Recycler並不保證池的大小,也就是說有多少物件就往池中加入多少物件,從而可能撐滿伺服器。通過在jvm啟動時加入-Dio.netty.recycler.maxCapacity.default=0引數來關閉Recycler池,前提:netty version>=4.0。所以可懷疑為記憶體洩露!
而對於Finalizer眾所周知的是,它會將要回收的物件進行一次挽救,所謂的挽救就是finalize方法裡面再對要回收的引用進行重新引用,finalize中的引用會加入ReferenceQueue中,再下次gc時檢查ReferenceQueue中的物件是否依然存在引用,沒有則進行回收。而Finalizer的垃圾回收執行緒級別非常低,在高併發情況下很難被gc到。仔細思考下Connection/J驅動中覆蓋finalize方法的目的是釋放連線,但導致了在併發高的情況下ReferenceQueue中大量引用沒被回收。所以可懷疑為記憶體洩露!
但是上面分析來說,還是懷疑,或者說是我們的基本類庫產生了一些我們可能不可探知的或不好調整的問題。其實如果控制好gc,有可能在業務物件空間中找回netty和jdbc產生的洩露。
後來想到了修改MAT的啟動配置引數,接下來說明一下MAT的記憶體分析過程。開啟MAT安裝目錄,下面會有一個MemoryAnalyzer.ini的配置檔案。該配置檔案完全是eclipse的配置檔案格式,跟eclipse配置方式相同,下面是我的配置引數:
-startup
../Eclipse/plugins/org.eclipse.equinox.launcher_1.3.100.v20150511-1540.jar
–launcher.library
../Eclipse/plugins/org.eclipse.equinox.launcher.cocoa.macosx.x86_64_1.1.300.v20150602-1417

-vmargs
-Xmx5g
-Xmx5g
-Xms5g
-server

-Dorg.eclipse.swt.internal.carbon.smallFonts
-XstartOnFirstThread

注意引用部分的是這次要加的引數。引數說明:
-vmargs:後面的引數都是jvm引數啦
-Xmx5g:最大堆記憶體5g
-Xms5g:啟動或最小堆記憶體為5g,最大最小配置相同防止抖動現象
-server:與tomcat的執行模式是相同的,C2級別的編譯優化,同時在也使用了併發gc等特性
在配置完MAT的堆記憶體引數後,來看看MAT匯入dump檔案分析的結果:

這裡寫圖片描述

MAT分析疑似出現了Memory Leak。
其中33.6M的由WebappClassLoader中引用了一些HashMap

這裡寫圖片描述

其中17.2M的由Finalizer產生

這裡寫圖片描述

剩餘24.7M的未知問題
Problem1:可以看到大量的JSF(京東的遠端呼叫框架)呼叫和Work呼叫執行緒,其實這是正常的業務請求。

這裡寫圖片描述

跟蹤執行緒棧,可以看到netty的nio執行緒。根據BHeapSampler的分析,以及Recycler存在的物件多記憶體洩露問題,可以想到如果jsf呼叫多了產生物件比較大,確實會產生記憶體洩露問題。不過根據這次dump檔案看到的jsf呼叫發現並不多。

這裡寫圖片描述

Problem2:下面兩個圖中可以看到在Finalizer佇列中,都是jdbc的東西,與上面用BHeapSampler分析的結果相同,證明可能是Connection/J產生的問題。

這裡寫圖片描述

這裡寫圖片描述

檢視裡面的深堆物件,可以看到成批的Finalizer,跟蹤gc root引用如此恐怖,並且下面的深堆數量也非常多,更加可以確定Connection/J產生了很大問題。

這裡寫圖片描述

這裡寫圖片描述

於是經過了上述過程的分析,so貌似得出了一個結論:業務程式碼沒主動產生記憶體洩露,只是業務請求量造成大量的物件,netty和jdbc導致的一些基本問題…而對於我們能做的來說,其實就是gc和記憶體配置不太好 - -….

這裡寫圖片描述

三、解決方案

解決方案就簡單了,配置一下gc和堆記憶體好了,我們線上服務的預設:
Young GC(PS Scavenge)
Full GC(PS MarkSweep)

這就很尷尬了,為何不是cms或者g1?那就改下吧!於是gc引數:
-XX:+UseConcMarkSweepGC //開啟cms gc
-XX:CMSInitiatingOccupancyFraction=80 //老年代佔用80%時進行full gc
-XX:+UseCMSInitiatingOccupancyOnly //讓cms gc老實一點,用我們的配置來工作
-XX:+CMSClassUnloadingEnabled // 永久代解除安裝類時進行cms gc操作
可以看到,產生堆滿的原因除了gc策略有問題外,還有一種可能:堆空間的老年代和年輕代的佔用比例不合適,而對於eden和survivor的比例也不合適,調整之:
-XX:SurvivorRatio=6 //survior佔用比例
-XX:NewRatio=4 //年輕代佔用比例
-XX:MaxDirectMemorySize=1024M //直接記憶體大小,這裡我們的jsf使用了netty,存在零拷貝,所以最好配置下直接記憶體
-XX:MaxTenuringThreshold=12 //物件晉升老年代年齡
-XX:PretenureSizeThreshold=5242880 //配置5M的大物件直接進入老年代,做分配擔保
同時為了節省記憶體,開啟了逃逸分析和標量替換
-XX:+DoEscapeAnalysis //開啟逃逸分析
-XX:+EliminateAllocations //開啟標量替換

整體配置:
-XX:SurvivorRatio=6
-XX:NewRatio=4
-XX:MaxDirectMemorySize=1024M
-XX:MaxTenuringThreshold=12
-XX:PretenureSizeThreshold=5242880
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=80
-XX:+UseCMSInitiatingOccupancyOnly
-XX:+CMSClassUnloadingEnabled
-XX:+DoEscapeAnalysis
-XX:+EliminateAllocations
注意這裡沒有配置-XX:+CMSParallelRemarkEnabled ,要是發現cms gc有卡頓延遲可以開啟。另外對於我們的jsf使用了netty,應該注意:
千萬不要開啟-XX:+DisableExplicitGC!千萬不要開啟-XX:+DisableExplicitGC!千萬不要開啟-XX:+DisableExplicitGC!(重要的事情說三遍) 因為netty要做System.gc()操作,而System.gc()會對直接記憶體進行一次標記回收,如果通過DisableExplicitGC禁用了,會導致netty產生的物件爆滿,boom shakalaka~
而-Dio.netty.recycler.maxCapacity.default=0引數上文只是提起了,並沒有最後加入虛擬機器引數中,因為剛接手該專案時間不長,不確定因素很多,在排查netty的Recycler的問題時看到專案pom.xml中有單獨引用netty包,懷疑是可能專案中除jsf之外另外使用了netty,不敢對其做太大變動。所以該引數需要等熟悉一段專案後再考慮是否加入。目前只使用了基本的jvm引數優化jvm。
接下來提交給架構師稽核了下,基本沒什麼問題,於是提交到運維實施。

四、後續結果(持續觀察中)

後續結果還不錯,直接上圖不解釋:
FULL GC(我們有了full gc!!!):

這裡寫圖片描述

YOUNG GC:

這裡寫圖片描述

HEAP:

這裡寫圖片描述

PERM:

這裡寫圖片描述

五、總結

jvm優化的過程任重而道遠,專案優化過程同樣任重而道遠。線上出現問題不怕,怕的是不出現問題,遇到問題才是好的,冷靜分析解決之,這就是進步的過程,人需要進步,專案亦需要進步。平時編寫程式碼嚴謹,儘量自測、互測,進行壓測,可以使用Chaos Monkey法則和Force Bot方法。jvm優化並不複雜,也不高深,推薦閱讀《深入理解jvm虛擬機器》《Java程式效能優化》,前者理論比較強,是理解java底層的必備,後者實踐操作比較強,涵蓋了jvm優化、故障分析、程式碼質量優化等很多內容。本次jvm調優經歷也是我對這兩本書的最佳實踐,給自己帶來很多感觸,在優化後看著記憶體穩定、gc合理回收、cpu也穩定了還是蠻有成就感的~特此寫在wiki上,希望大家遇到了這樣的問題能有所參考。