1.系統(tǒng)背景
這是當時開發(fā)中遇到的一個真實場景伙单,也是大部分人在開發(fā)項目中有可能會遇到的一些場景,該系統(tǒng)主要是做大數(shù)據(jù)相關(guān)計算分析的哈肖,日處理數(shù)據(jù)量在上億的規(guī)模。這里我們重點針對JVM內(nèi)存的管理來進行模型分析念秧,數(shù)據(jù)的來源獲取主要是MYSQL數(shù)據(jù)庫以及其他數(shù)據(jù)源里提取大量的數(shù)據(jù)淤井,通過加載到JVM內(nèi)存的過程我們來一起分析出現(xiàn)的問題以及如何優(yōu)化解決(如下圖所示):
2.生產(chǎn)環(huán)境
這是一套分布式運行系統(tǒng),生產(chǎn)環(huán)境部署了多臺服務(wù)器(每臺4核8G配置)摊趾,每臺機器大概每分鐘負責執(zhí)行100次數(shù)據(jù)提取和計算币狠,每次提取大概1萬條左右的數(shù)據(jù)到內(nèi)存計算,平均每次計算需要耗費10秒左右時間砾层。 JVM內(nèi)存總共分配了4G南捂,堆內(nèi)存占3G甫菠,其中新生代和老年代分別是1.5G的內(nèi)存空間
3.過程分析
按照上述的背景和實際生產(chǎn)環(huán)境,那每次1萬條數(shù)據(jù)會占用多少的內(nèi)存空間呢?這里每條數(shù)據(jù)較大吱晒,平均包含20個字段,可以認為每條數(shù)據(jù)大概在1KB左右怠惶。那么1萬條數(shù)據(jù)對應(yīng)就是10MB大小回季。那么運行多久就會導(dǎo)致新生代塞滿呢?
新生代總共分配1.5G秕重,那么Eden區(qū)分配就是1.2G不同,S1和S2區(qū)分別是150MB;如下圖:
現(xiàn)在我們可以來手動計算下了溶耘,1次往Eden區(qū)里填充10MB對象二拐,1分鐘讀取100次,也就是差不多1個G凳兵,那也就是1分鐘左右的時候我們的Eden區(qū)就差不多填滿了百新,這個時候如果觸發(fā)Minor GC,我們通過上文學習知道留荔,JVM在執(zhí)行Minor GC之前是會進行一步檢查動作的:老年代可用內(nèi)存空間是否大于新生代全部對象吟孙?如果是第一次運行到這兒,那么我們的老年代是空的聚蝶,也就是有1.5G的空間杰妓,完全是夠用的。
這里觸發(fā)Minor GC進行回收碘勉,但是問題在于如何回收呢巷挥?我們重點來看每次任務(wù)計算的耗時是10S,這里差不多有80次的任務(wù)都已經(jīng)執(zhí)行完畢了验靡,還有大概20個任務(wù)正在計算中倍宾,也就是對應(yīng)還有200MB的對象在引用著雏节,這部分對象是不會被回收的,而我們的幸存者區(qū)域最大也就是150MB無法存放下200MB高职,那么根據(jù)我們講過的空間擔保機制钩乍,這200MB對象會直接進入到老年代!
由于每一分鐘就會將Eden區(qū)填滿觸發(fā)Minor GC怔锌,也就是每分鐘就會有200MB對象進入到老年代寥粹,那當老年代的內(nèi)存占用的越多后會發(fā)生什么事情呢?比如兩分鐘過去了埃元,這時占用400MB涝涤,那老年代可用空間就只剩1.1G了,那第三分鐘觸發(fā)Minor GC的時候岛杀,一判斷發(fā)現(xiàn)阔拳,老年代剩余空間已小于Eden區(qū)所有對象1.2G大小了,則會走另一條分支的判斷了类嗤,我們可以根據(jù)下圖再來回顧下:
先看參數(shù):-XX:-HandlePromotionFailure是否設(shè)置糊肠,當然一般都會設(shè)置,此時會判斷老年代連續(xù)空間是否大于歷史平均晉升老年代對象的大小遗锣,那歷史晉升對象大小都在200MB罪针,很明顯大于,那么JVM會直接進行冒險操作黄伊,觸發(fā)Minor GC的執(zhí)行泪酱,而本次冒險是成功的!新生代依然繼續(xù)晉升200MB對象到老年代还最。
那么當系統(tǒng)運行到第7分鐘的時候墓阀,這時進入到老年代的對象有1.4G了,剩余空間僅剩100MB拓轻!如下圖:
系統(tǒng)運行到這兒斯撮,發(fā)現(xiàn)老年代剩余空間已經(jīng)比歷史平均晉升對象大小都要小了,這時會直接觸發(fā)Full GC扶叉!假設(shè)老年代空間都可以被回收勿锅,那么這時老年代對象就完全清除,接著會繼續(xù)進行Minor GC枣氧,200MB對象繼續(xù)進入老年代溢十,又開始重復(fù)循環(huán)執(zhí)行了。
那么按照以上的運行分析达吞,我們可以得出一個結(jié)論就是:系統(tǒng)平均運行7张弛、8分鐘左右就會觸發(fā)一次Full GC的執(zhí)行!而每次一旦Full GC執(zhí)行,就會嚴重影響到系統(tǒng)的運行效率吞鸭,加上該系統(tǒng)的Full GC頻率較高寺董,給用戶帶來的使用感受是非常糟糕的!
4.JVM優(yōu)化
像真實開發(fā)中大家也有很大幾率會遇到類似這樣的情況刻剥,我們應(yīng)該減少Full GC的次數(shù)以及降低它出現(xiàn)的頻率遮咖,甚至不觸發(fā)Full GC,那么如何進行優(yōu)化呢造虏?這也是考驗一個Java程序員的價值體現(xiàn)盯滚。
針對類似的計算系統(tǒng),每次Minor GC的時候酗电,必然會有一部分數(shù)據(jù)沒處理完畢,但是按照現(xiàn)有的內(nèi)存模型内列,我們的幸存者區(qū)域只有150MB是無法滿足200MB對象的存放撵术,因此有必要調(diào)整我們的內(nèi)存比例。
解決方案:
3GB的堆內(nèi)存大小话瞧,我們直接分配2G給新生代嫩与,1G給老年代,這樣Survivor區(qū)的大小就有200MB了每次剛好能存放下MinorGC過后存活的對象了交排。如下圖所示:
只要每次Minor GC時200MB存活對象可以存放進Survivor區(qū)划滋,那么等下一次Minor GC時這部分對象對應(yīng)的計算任務(wù)也已經(jīng)結(jié)束,也可以直接進行回收埃篓。
那么接下來我們還是在繼續(xù)模擬跑一次处坪,當Eden區(qū)內(nèi)存已經(jīng)裝滿,此時S0區(qū)也有200MB對象架专,這是觸發(fā)Minor GC的執(zhí)行同窘,200MB正在執(zhí)行的任務(wù)對象(存活對象)直接轉(zhuǎn)移到S1區(qū),回收清空掉Eden區(qū)和S0區(qū)部脚,如下圖:
那么通過以上的分析也不免看出想邦,基本上很少會有對象進入到老年代,我們也成功的將幾分鐘一次的Full GC降低到幾個小時一次委刘,大幅度提升了系統(tǒng)的性能丧没,避免了Full GC對系統(tǒng)運行的影響!
當然這里其實還有一個細節(jié)點:就是動態(tài)年齡對象規(guī)則锡移!如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半呕童,年齡大于或等于該年齡的對象就可以直接進入老年代,無須等到-XX:MaxTenuringThreshold中要求的年齡淆珊。這里需要結(jié)合自己公司的實際系統(tǒng)分析到底有多少對象是根據(jù)動態(tài)年齡規(guī)則進入到了老年代拉庵,如果要避免因為這項規(guī)則進入老年代,從而觸發(fā)Full GC也可以嘗試調(diào)整Eden區(qū)和Survivor區(qū)的比例,調(diào)整survivor區(qū)的大小钞支。