通過GC輸出分析內存泄露問題

SIP5.0以後服務的請求量爆發性增長,因此也暴露了原來沒有暴露出來的問題。由於過去一般一個新版本發佈週期在一個月左右,因此如果是小的內存泄露,在一個月之內重新發布以後也就看不出任何問題。

因此這陣子除了優化Memcache客戶端和SIP框架邏輯以外其他依賴部分以外,對於內存泄露的壓力測試也開始實實在在的做起來。經過這次問題的定位和解決以後,大致覺得對於一個大用戶量應用要放心的話,那麼需要做這麼幾步。

1.       在GC輸出的環境下,大壓力下做多天的測試。(可以在 JAVA_OPTS增加-verbose:gc -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError)

2.       檢查GC輸出日誌來判斷是否有內存泄露。(這部分後面有詳細的實例說明)

3.       如果出現內存泄露問題,則使用jprofiler等工具來排查內存泄露點(之所以不一開始使用,因爲jprofiler等工具對於壓力測試有影響,使得大壓力無法上去,也使問題不那麼容易暴露)

4.       解決問題,並在重複2步驟。

這裏對SIP在jdk1.5和jdk1.6下做壓力測試的GC 日誌來做一個實際的分析對比,通過對比來大致描述一下如何根據輸出情況能夠了解應用是否存在內存泄露問題。(這裏的內存泄露問題就是在以前blog寫過的jdk的concurrent包內LinkedBlockingQueue的poll方法存在比較嚴重的內存泄露,調用頻率越高,內存泄露的越厲害)

兩次壓力測試都差不多都是兩天,測試方案如下:

開始50個併發,每個併發每次請求完畢後休息0.1秒,10分鐘後增長50個併發,按此規律增長到500併發。

舊版本SIP是在JDK1.5環境下完成的壓力測試,

新版本SIP的JDK版本是1.6,

壓力機和以前一樣,是10.2.226.40,DELL1950,8CPU,8G內存。

壓力機模擬發出對一個需要簽名的API不斷的調用請求。

看看兩個Log的具體內容(內容很多截取部分做分析)

先說一下日誌輸出的結構:(1.6和1.5略微有一些不同,只是1.6對於時間統計更加細緻)

[GC [<collector>: <starting occupancy1> -> <ending occupancy1>, <pause time1> secs] <starting occupancy3> -> <ending occupancy3>, <pause time3> secs]

<collector>GC收集器的名稱

<starting occupancy1> 新生代在GC前佔用的內存

<ending occupancy1> 新生代在GC後佔用的內存

<pause time1> 新生代局部收集時jvm暫停處理的時間

<starting occupancy3> JVM Heap 在GC前佔用的內存

<ending occupancy3> JVM Heap 在GC後佔用的內存

<pause time3> GC過程中jvm暫停處理的總時間

Jdk1.5 log:

啓動時GC輸出:

[GC [DefNew: 209792K->4417K(235968K), 0.0201630 secs] 246722K->41347K(498112K), 0.0204050 secs]

[GC [DefNew: 214209K->4381K(235968K), 0.0139200 secs] 251139K->41312K(498112K), 0.0141190 secs]

一句輸出:

新生代回收前209792K,回收後4417K,回收數量205375K,Heap總量回收前246722K回收後41347K,回收總量205375K。這就表示100%的收回,沒有任何新生代的對象被提升到中生代或者永久區(名字說的不一定準確,只是表達意思)。

第二句輸出:

按照分析也就只是有1K內容被提升到中生代。

運行一段時間後:

[GC [DefNew: 210686K->979K(235968K), 0.0257140 secs] 278070K->68379K(498244K), 0.0261820 secs]

[GC [DefNew: 210771K->1129K(235968K), 0.0275160 secs] 278171K->68544K(498244K), 0.0280050 secs]

第一句輸出:

         新生代回收前210686K,回收後979K,回收數量209707K,Heap總量回收前278070K回收後68379K,回收總量209691K。這就表示有16k沒有被回收。

第二句輸出:

         新生代回收前210771K,回收後1129K,回收數量209642K,Heap總量回收前278171K回收後68544K,回收總量209627K。這就表示有15k沒有被回收。

比較一下啓動時與現在的新生代佔用內存情況和Heap使用情況發現Heap的使用增長很明顯,新生代沒有增長,而Heap使用總量增長了27M,這就表明可能存在內存泄露,雖然每一次泄露的字節數很少,但是頻率很高,大部分泄露的對象都被升級到了中生代或者持久代。

又一段時間後:

[GC [DefNew: 211554K->1913K(235968K), 0.0461130 secs] 350102K->140481K(648160K), 0.0469790 secs]

[GC [DefNew: 211707K->2327K(235968K), 0.0546170 secs] 350275K->140921K(648160K), 0.0555070 secs]

第一句輸出:

         新生代回收前211554K,回收後1913K,回收數量209641K,Heap總量回收前350102K回收後140481K,回收總量209621K。這就表示有20k沒有被回收。



         分析到這裏就可以看出每一次泄露的內存只有10幾K,但是在大壓力長時間的測試下,內存泄露還是很明顯的,此時Heap已經增長到了140M,較啓動時已經增長了100M。同時GC佔用的時間越來越長。

後續的現象:

         後續觀察日誌會發現,Full GC的頻率越來越高,收集所花費時間也是越來越長。(Full GC定期會執行,同時局部回收不能滿足分配需求的情況下也會執行)。



[Full GC [Tenured: 786431K->786431K(786432K), 3.4882390 secs] 1022399K->1022399K(1022400K), [Perm : 36711K->36711K(98304K)], 3.4887920 secs]

java.lang.OutOfMemoryError: Java heap space

Dumping heap to java_pid7720.hprof ...



         出現這個語句表示內存真的被消耗完了。

Jdk1.6 log:



啓動時GC的輸出:

[GC [PSYoungGen: 221697K->31960K(229376K)] 225788K->36051K(491520K), 0.0521830 secs] [Times: user=0.26 sys=0.05, real=0.05 secs]

[GC [PSYoungGen: 228568K->32752K(229376K)] 232659K->37036K(491520K), 0.0408620 secs] [Times: user=0.21 sys=0.02, real=0.04 secs]



第一句輸出:

         新生代回收前221697K,回收後31960K,回收數量189737K,Heap總量回收前225788K回收後36051K,回收總量189737K。100%被回收。



運行一段時間後輸出:

[GC [PSYoungGen: 258944K->2536K(259328K)] 853863K->598135K(997888K), 0.0471620 secs] [Times: user=0.15 sys=0.00, real=0.05 secs]

[GC [PSYoungGen: 259048K->2624K(259328K)] 854647K->598907K(997888K), 0.0462980 secs] [Times: user=0.16 sys=0.02, real=0.04 secs]



第一句輸出:

         新生代回收前258944K,回收後2536K,回收數量256408K,Heap總量回收前853863K回收後598135K,回收總量255728K。680K沒有被回收,但這並不意味着就會產生內存泄露。同時可以看出GC回收時間並沒有增加。



在運行一段時間後輸出:

[GC [PSYoungGen: 258904K->2488K(259264K)] 969663K->713923K(1045696K), 0.0485140 secs] [Times: user=0.16 sys=0.01, real=0.04 secs]

[GC [PSYoungGen: 258872K->2448K(259328K)] 970307K->714563K(1045760K), 0.0473770 secs] [Times: user=0.16 sys=0.01, real=0.05 secs]



第一句輸出:

         新生代回收前258904K,回收後2488K,回收數量256416K,Heap總量回收前969663K回收後713923K,回收總量255740K。676K沒有被回收,同時總的Heap也有所增加。

         此時看起來好像和1.5的狀況一樣。但是查看了一下Full GC的執行還是400-500次GC執行一次,因此繼續觀察。



運行一天多以後輸出:

[GC [PSYoungGen: 257016K->3304K(257984K)] 1019358K->766310K(1044416K), 0.0567120 secs] [Times: user=0.18 sys=0.01, real=0.06 secs]

[GC [PSYoungGen: 257128K->2920K(258112K)] 1020134K->766622K(1044544K), 0.0549570 secs] [Times: user=0.19 sys=0.00, real=0.05 secs]



可以發現Heap增長趨緩。



運行兩天以後輸出:

[GC [PSYoungGen: 256936K->3584K(257792K)] 859561K->606969K(1044224K), 0.0565910 secs] [Times: user=0.18 sys=0.01, real=0.06 secs]

[GC [PSYoungGen: 256960K->3368K(257728K)] 860345K->607445K(1044160K), 0.0553780 secs] [Times: user=0.18 sys=0.01, real=0.06 secs]



發現Heap反而減少了,此時可以對內存泄露問題作初步排除了。(其實在jdk1.6環境下用jprofiler來觀察,對於concurrent那個內存泄露點的跟蹤發現,內存的確還是會不斷增長的,不過在一段時間後還是有回收,因此也就可以部分解釋前面出現的情況)



總結:

         對於GC輸出的觀察需要分兩個維度來看。一個是縱向比較,也就是一次回收對於內存變化的觀察。一個是橫向比較,對於長時間內存分配佔用情況的比較,這部分比較需要較長時間的觀察,不能僅僅憑短時間的幾個抽樣比較,因爲對於抽樣來說,Full GC前後的區別,運行時長的區別,資源瞬時佔用的區別都會影響判斷。同時要結合Full GC發生的時間週期,每一次GC收集所耗費的時間作爲輔助判斷標準。

         順便說一下,Heap的 YoungGen,OldGen,PermGen的設置也是需要注意的,並不是越大越好,越大執行收集的時間越久,但是可能執行Full GC的頻率會比較低,因此需要權衡。這些仔細的去了解一下GC的基礎設計思想會更有幫助,不過一般用默認的也不錯。還有就是可以配置一些特殊的GC,並行,同步等等,充分利用多CPU的資源。

         對於GC的優化可以通過現在很多圖形工具來做,也可以類似於我這樣採用最原始的分析方式,好處就是任何時間任何地點只要知道原理就可以分析無需藉助外部工具。原始的總是最好的^_^。

發佈了29 篇原創文章 · 獲贊 10 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章