【案例】
最近我的同學(xué)遇到一個線上問題,線上機器的jvm進程頻繁FullGC(大約每10分鐘一次),登陸機器發(fā)現(xiàn)jvm參數(shù)只配置了最大堆內(nèi)存,其他配置都是系統(tǒng)默認配置,問我如何排查并優(yōu)化?
【特征】
經(jīng)過排查得知,該機器上部署的業(yè)務(wù)會產(chǎn)生大量的JSON對象,并且每個JSON對象大小大約在1M~10M之間,但是JSON對象的作用域很短。
在JVM啟動參數(shù)中,可以設(shè)置內(nèi)存、垃圾回收相關(guān)的一些參數(shù)設(shè)置,默認情況不做任何設(shè)置JVM會工作的很好,但對一些配置很好的Server和具體的應(yīng)用必須仔細調(diào)優(yōu)才能獲得最佳性能。
本次事故的原因是由于使用了JVM默認配置:年輕代和年老代比例為1:2,將年輕代和年老代比例調(diào)整為1:1,F(xiàn)ullGC 頻率明顯降低了,業(yè)務(wù)停頓時間大大縮短。
【分析】
對象的動態(tài)年齡判斷:
為了能更好地適應(yīng)不同程序的內(nèi)存狀況,虛擬機并不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡
—–《深入理解Java虛擬機》
由于存在大量的新生對象,而年輕代設(shè)置相對較小,根據(jù)對象的動態(tài)年齡判斷,對象在年輕代提前晉升到老年代,使得老年代迅速增大,又由于老年代的GC算法采用標記清除算法,會產(chǎn)生大量內(nèi)存碎片,使得老年代剩余空間很大但是沒有足夠大的連續(xù)空間分配給當前對象,而不得不提前觸發(fā)FullGC。
【總結(jié)】
JVM參數(shù)調(diào)優(yōu)的目的:
* GC的時間足夠的小
* GC的次數(shù)足夠的少
* 發(fā)生Full GC的周期足夠的長
前兩個目前是相悖的,要想GC時間小必須要一個更小的堆,要保證GC次數(shù)足夠少,必須保證一個更大的堆,我們只能取其平衡。
(1)針對JVM堆的設(shè)置,一般可以通過-Xms -Xmx限定其最小、最大值,為了防止垃圾收集器在最小、最大之間收縮堆而產(chǎn)生額外的時間,我們通常把最大、最小設(shè)置為相同的值
(2)年輕代和年老代將根據(jù)默認的比例(1:2)分配堆內(nèi)存,可以通過調(diào)整二者之間的比率NewRadio來調(diào)整二者之間的大小,也可以針對回收代,比如年輕代,通過 -XX:newSize -XX:MaxNewSize來設(shè)置其絕對大小。同樣,為了防止年輕代的堆收縮,我們通常會把-XX:newSize -XX:MaxNewSize設(shè)置為同樣大小
(3)年輕代和年老代設(shè)置多大才算合理?這個我問題毫無疑問是沒有答案的,否則也就不會有調(diào)優(yōu)。我們觀察一下二者大小變化有哪些影響
* 更大的年輕代必然導(dǎo)致更小的年老代,大的年輕代會延長普通GC的周期,但會增加每次GC的時間;小的年老代會導(dǎo)致更頻繁的Full GC
* 更小的年輕代必然導(dǎo)致更大年老代,小的年輕代會導(dǎo)致普通GC很頻繁,但每次的GC時間會更短;大的年老代會減少Full GC的頻率
* 如何選擇應(yīng)該依賴應(yīng)用程序對象生命周期的分布情況:如果應(yīng)用存在大量的臨時對象,應(yīng)該選擇更大的年輕代;如果存在相對較多的持久對象,年老代應(yīng)該適當增大。但很多應(yīng)用都沒有這樣明顯的特性,在抉擇時應(yīng)該根據(jù)以下兩點:(A)本著Full GC盡量少的原則,讓年老代盡量緩存常用對象,JVM的默認比例1:2也是這個道理 (B)通過觀察應(yīng)用一段時間,看其他在峰值時年老代會占多少內(nèi)存,在不影響Full GC的前提下,根據(jù)實際情況加大年輕代,比如可以把比例控制在1:1。但應(yīng)該給年老代至少預(yù)留1/3的增長空間。