1. 程式人生 > >java-jvm-full gc頻繁的分析及解決

java-jvm-full gc頻繁的分析及解決

關於應用full gc頻繁的分析及解決 
很久前的工作日記了,移到ITeye上來。

現象

系統報警full gc次數過多,每2分鐘達到了5~6次,這是不正常的現象
在full gc報警時的gc.log如下:
在full gc報警時的jstat如下:
sudo -u admin -H /opt/taobao/java/bin/jstat -gcutil `pgrep java` 2000 100
此時的cpu如下(基本都是在做gc):
將應用重啟後,問題解決
但是當後臺執行低價航線更新時,過大概十幾個小時後,又出現上述情況!

分析

當頻繁full gc時,jstack打印出堆疊資訊如下:
sudo -u admin -H /opt/taobao/java/bin/jstack `pgrep java` > #your file path#
可以看到的確是在跑低價資訊
另外在應用頻繁full gc時和應用正常時,也執行了如下2種命令:
sudo -u admin -H /opt/taobao/java/bin/jmap -histo `pgrep` > #your file path#
sudo -u admin -H /opt/taobao/java/bin/jmap -histo:live `pgrep` > #your file path#(live會產生full gc)
目的是確認以下2種資訊:
(1)是否存在某些引用的不正常,造成物件始終可達而無法回收(Java中的記憶體洩漏)
(2)是否真是由於在頻繁full gc時同時又有大量請求進入分配記憶體從而處理不過來,
        造成concurrent mode failure?
下圖是在應用正常情況下,jmap不加live,產生的histo資訊:
下圖是在應用正常情況下,jmap加live,產生的histo資訊:
下圖是在應用頻繁full gc情況下,jmap不加live和加live,產生的histo資訊:
從上述幾個圖中可以看到:
(1)在應用正常情況下,圖中標紅的物件是被回收的,因此不是記憶體洩漏問題
(2)在應用頻繁full gc時,標紅的物件即使加live也是未被回收的,因上就是在頻繁full gc時,
        同時又有大量請求進入分配記憶體從而處理不過來的問題

先從解決問題的角度,看怎樣造成頻繁的full gc?

從分析CMS GC開始
先給個CMS
GC的概況:
(1)young gc
可以看到,當eden滿時,young gc使用的是ParNew收集器
ParNew: 2230361K->129028K(2403008K), 0.2363650 secs解釋:
12230361K->129028K,指回收前後eden+s1(或s2)大小
2)2403008K,指可用的young代的大小,即eden+s1(或s2)
3)0.2363650 secs,指消耗時間
2324774K->223451K(3975872K), 0.2366810 sec解釋:
12335109K->140198K,指整個堆大小的變化
(heap=(young+old)+perm;young=eden+s1+s2;s1=s2=young/(survivor ratio+2))
2)0.2366810 sec,指消耗時間
[Times: user=0.60 sys=0.02, real=0.24 secs]解釋:指使用者時間,系統時間,真實時間
2cms gc
當使用CMS收集器時,當開始進行收集時,old代的收集過程如下所示:
a)首先jvm根據-XX:CMSInitiatingOccupancyFraction-XX:+UseCMSInitiatingOccupancyOnly
     來決定什麼時間開始垃圾收集
b)如果設定了-XX:+UseCMSInitiatingOccupancyOnly,那麼只有當old代佔用確實達到了
     -XX:CMSInitiatingOccupancyFraction引數所設定的比例時才會觸發cms gc
c)如果沒有設定-XX:+UseCMSInitiatingOccupancyOnly,那麼系統會根據統計資料自行決定什麼時候
    觸發cms gc;因此有時會遇到設定了80%比例才cms gc,但是50%時就已經觸發了,就是因為這個引數
    沒有設定的原因
d)當cms gc開始時,首先的階段是CMS-initial-mark,此階段是初始標記階段,是stop the world階段,
     因此此階段標記的物件只是從root集最直接可達的物件
CMS-initial-mark:961330K(1572864K),指標記時,old代的已用空間和總空間
e)下一個階段是CMS-concurrent-mark,此階段是和應用執行緒併發執行的,所謂併發收集器指的就是這個,
     主要作用是標記可達的物件
此階段會列印2條日誌:CMS-concurrent-mark-start,CMS-concurrent-mark
f)下一個階段是CMS-concurrent-preclean,此階段主要是進行一些預清理,因為標記和應用執行緒是併發執行的,
    因此會有些物件的狀態在標記後會改變,此階段正是解決這個問題
因為之後的Rescan階段也會stop the world,為了使暫停的時間儘可能的小,也需要preclean階段先做一部分
    工作以節省時間
此階段會列印2條日誌:CMS-concurrent-preclean-start,CMS-concurrent-preclean
g)下一階段是CMS-concurrent-abortable-preclean階段,加入此階段的目的是使cms gc更加可控一些,
     作用也是執行一些預清理,以減少Rescan階段造成應用暫停的時間
此階段涉及幾個引數:
-XX:CMSMaxAbortablePrecleanTime:當abortable-preclean階段執行達到這個時間時才會結束
-XX:CMSScheduleRemarkEdenSizeThreshold(預設2m):控制abortable-preclean階段什麼時候開始執行,
即當eden使用達到此值時,才會開始abortable-preclean階段
-XX:CMSScheduleRemarkEdenPenetratio(預設50%):控制abortable-preclean階段什麼時候結束執行
此階段會列印一些日誌如下:
CMS-concurrent-abortable-preclean-start,CMS-concurrent-abortable-preclean,
CMS:abort preclean due to time XXX
h)再下一個階段是第二個stop the world階段了,即Rescan階段,此階段暫停應用執行緒,對物件進行重新掃描並
     標記
YG occupancy:964861K2403008K),指執行時young代的情況
CMS remark:961330K(1572864K),指執行時old代的情況
此外,還打印出了弱引用處理、類解除安裝等過程的耗時
i)再下一個階段是CMS-concurrent-sweep,進行併發的垃圾清理
j)最後是CMS-concurrent-reset,為下一次cms gc重置相關資料結構
(3)full gc:
有2種情況會觸發full gc,在full gc時,整個應用會暫停
a)concurrent-mode-failure:當cms gc正進行時,此時有新的物件要進行old代,但是old代空間不足造成的
b)promotion-failed:當進行young gc時,有部分young代物件仍然可用,但是S1或S2放不下,
    因此需要放到old代,但此時old代空間無法容納此
頻繁full gc的原因
從日誌中可以看出有大量的concurrent-mode-failure,因此正是當cms gc進行時,有新的物件要進行old代,
但是old代空間不足造成的full gc
程序的jvm引數如下所示:
影響cms gc時長及觸發的引數是以下2個:
-XX:CMSMaxAbortablePrecleanTime=5000
-XX:CMSInitiatingOccupancyFraction=80
解決也是針對這兩個引數來的
根本的原因是每次請求消耗的記憶體量過大

解決

1)針對cms gc的觸發階段,調整-XX:CMSInitiatingOccupancyFraction=50,提早觸發cms gc,就可以
        緩解當old代達到80%,cms gc處理不完,從而造成concurrent mode failure引發full gc
2)修改-XX:CMSMaxAbortablePrecleanTime=500,縮小CMS-concurrent-abortable-preclean階段
        的時間
3)考慮到cms gc時不會進行compact,因此加入-XX:+UseCMSCompactAtFullCollection
       (cms gc後會進行記憶體的compact)和-XX:CMSFullGCsBeforeCompaction=4
       (在full gc4次後會進行compact)引數
但是運行了一段時間後,只不過時間更長了,又會出現頻繁full gc
計算了一下heap各個代的大小(可以用jmap -heap檢視):
total heap=young+old=4096m
perm:256m
young=s1+s2+eden=2560m
young avail=eden+s1=2133.375+213.3125=2346.6875m
s1=2560/(10+1+1)=213.3125m
s2=s1
eden=2133.375m
old=1536m
可以看到eden大於old,在極端情況下(young區的所有物件全都要進入到old時,就會觸發full gc),
因此在應用頻繁full gc時,很有可能old代是不夠用的,因此想到將old代加大,young代減小
改成以下:
-Xmn1920m
新的各代大小:
total heap=young+old=4096m
perm:256m
young=s1+s2+eden=1920m
young avail=eden+s1=2133.375+213.3125=1760m
s1=1760/(10+1+1)=160m
s2=s1
eden=1600m
old=2176m
此時的eden小於old,可以緩解一些問題