FullGC 40次/天到10天1次,真牛?。?!
兄弟們,咱們程序員對 GC(Garbage Collection,垃圾回收)真是又愛又恨。愛的是它幫你自動管理內(nèi)存,不用像 C++ 程序員那樣手動釋放內(nèi)存,恨的是它一旦抽風(fēng),頻繁觸發(fā) FullGC,整個系統(tǒng)就像被踩了急剎車,用戶體驗直接崩盤。
我有個朋友,最近就遇到了這么個糟心事。他們公司的線上系統(tǒng),每天要觸發(fā) 40 多次 FullGC,每次 FullGC 都要卡頓好幾秒,用戶投訴像雪片一樣飛來。老板拍桌子說:“再搞不定,你們都去喝西北風(fēng)!”
沒辦法,朋友只能硬著頭皮上。他打開 GC 日志一看,好家伙,老年代內(nèi)存使用率一直居高不下,每次 FullGC 后老年代內(nèi)存都只降了一點點,這明顯是內(nèi)存泄漏的節(jié)奏??!
一、內(nèi)存泄漏:程序員的 “漏風(fēng)棉襖”
內(nèi)存泄漏就像棉襖漏風(fēng),剛開始你可能沒感覺,但時間一長,寒氣就會慢慢侵入你的身體。同樣,內(nèi)存泄漏剛開始可能不會對系統(tǒng)造成明顯影響,但隨著時間的推移,內(nèi)存會被慢慢耗盡,最終導(dǎo)致系統(tǒng)崩潰。
朋友通過分析堆轉(zhuǎn)儲文件(Heap Dump),發(fā)現(xiàn)有一個靜態(tài)集合類持有了大量的對象,這些對象本應(yīng)該在方法執(zhí)行完畢后就被回收,但由于靜態(tài)集合的引用,它們一直無法被釋放。這就像你把一件舊衣服放在衣柜里,雖然你不再穿它,但它占用了衣柜的空間,導(dǎo)致新衣服沒地方放。
找到了問題所在,朋友果斷修改了代碼,不再使用靜態(tài)集合,而是改用線程局部變量(ThreadLocal)。這樣,每個線程都有自己的集合,方法執(zhí)行完畢后,集合中的對象就可以被自動回收了。
二、參數(shù)調(diào)優(yōu):像調(diào)咖啡一樣調(diào) JVM
解決了內(nèi)存泄漏問題,朋友以為 FullGC 的問題就解決了,沒想到還是每天觸發(fā) 20 多次。這時候,他意識到可能需要調(diào)整 JVM 參數(shù)了。
JVM 參數(shù)調(diào)優(yōu)就像調(diào)咖啡,不同的參數(shù)組合會產(chǎn)生不同的效果。朋友根據(jù)系統(tǒng)的特點,調(diào)整了以下參數(shù):
- 堆內(nèi)存大小:將初始堆內(nèi)存(-Xms)和最大堆內(nèi)存(-Xmx)都設(shè)置為 8G,避免 JVM 在運(yùn)行過程中動態(tài)調(diào)整堆大小,減少性能波動。
- 年輕代大小:將年輕代(-Xmn)設(shè)置為 2G,占整個堆內(nèi)存的 25%。年輕代越大,Minor GC 的頻率就越低,對象晉升到老年代的概率也越小。
- 垃圾收集器:啟用 G1 垃圾收集器(-XX:+UseG1GC),G1 收集器采用分區(qū)收集算法,能夠更好地控制停頓時間,尤其適合大內(nèi)存場景。
- 最大 GC 停頓時間:設(shè)置 - XX:MaxGCPauseMillis=200,告訴 G1 收集器,每次垃圾回收的停頓時間不要超過 200 毫秒。
調(diào)整完參數(shù)后,朋友滿心歡喜地部署了系統(tǒng),結(jié)果 FullGC 還是每天觸發(fā) 10 多次。這時候,他意識到可能還有其他問題。
三、數(shù)據(jù)庫查詢:FullGC 的 “隱形殺手”
朋友仔細(xì)檢查了系統(tǒng)的日志,發(fā)現(xiàn)每次 FullGC 前,數(shù)據(jù)庫查詢的耗時都明顯增加。原來,系統(tǒng)中有一個接口,每次查詢都會返回大量的數(shù)據(jù),這些數(shù)據(jù)在內(nèi)存中處理時,導(dǎo)致年輕代內(nèi)存迅速填滿,對象頻繁晉升到老年代,從而觸發(fā) FullGC。
為了解決這個問題,朋友對數(shù)據(jù)庫查詢進(jìn)行了優(yōu)化:
- 加索引:對查詢條件字段添加索引,提高查詢效率。
- 分頁查詢:將一次查詢大量數(shù)據(jù)改為分頁查詢,每次只返回一頁數(shù)據(jù),減少內(nèi)存占用。
- 批量操作:將多次單條插入改為批量插入,減少數(shù)據(jù)庫交互次數(shù)。
優(yōu)化完數(shù)據(jù)庫查詢后,F(xiàn)ullGC 的頻率明顯降低了,每天只觸發(fā) 5 次左右。但朋友并不滿足,他想要徹底解決 FullGC 的問題。
四、并發(fā)編程:FullGC 的 “加速器”
朋友發(fā)現(xiàn),系統(tǒng)中有一個異步任務(wù),每次執(zhí)行都會創(chuàng)建大量的對象,這些對象在年輕代中無法及時回收,導(dǎo)致年輕代內(nèi)存迅速填滿,觸發(fā) Minor GC。而 Minor GC 后,仍然有大量的對象存活,晉升到老年代,從而觸發(fā) FullGC。
為了解決這個問題,朋友對異步任務(wù)進(jìn)行了優(yōu)化:
- 線程池優(yōu)化:調(diào)整線程池的核心線程數(shù)和最大線程數(shù),避免線程頻繁創(chuàng)建和銷毀。
- 對象復(fù)用:復(fù)用已經(jīng)創(chuàng)建的對象,減少對象的創(chuàng)建和銷毀次數(shù)。
- 異步處理:將一些非關(guān)鍵業(yè)務(wù)邏輯改為異步處理,避免在主線程中創(chuàng)建大量對象。
優(yōu)化完異步任務(wù)后,F(xiàn)ullGC 的頻率進(jìn)一步降低,每天只觸發(fā) 2 次左右。但朋友還是覺得不夠,他想要做到 10 天觸發(fā)一次 FullGC。
五、JVM 監(jiān)控:FullGC 的 “照妖鏡”
朋友使用 Prometheus 和 Grafana 搭建了 JVM 監(jiān)控系統(tǒng),實時監(jiān)控 JVM 的內(nèi)存使用情況、GC 頻率、線程狀態(tài)等指標(biāo)。通過監(jiān)控,他發(fā)現(xiàn)系統(tǒng)中存在一些大對象,這些大對象直接進(jìn)入老年代,導(dǎo)致老年代內(nèi)存迅速填滿,觸發(fā) FullGC。
為了解決這個問題,朋友對大對象進(jìn)行了優(yōu)化:
- 對象拆分:將大對象拆分成多個小對象,減少單個對象的內(nèi)存占用。
- 緩存優(yōu)化:使用緩存緩存頻繁訪問的大對象,避免重復(fù)創(chuàng)建。
- 內(nèi)存分配策略:調(diào)整 JVM 的內(nèi)存分配策略,讓大對象盡可能在年輕代中分配。
優(yōu)化完大對象后,F(xiàn)ullGC 的頻率終于降到了 10 天一次。朋友的老板高興得合不攏嘴,說要給他漲工資。
六、總結(jié):FullGC 優(yōu)化的 “葵花寶典”
通過這次 FullGC 優(yōu)化,朋友總結(jié)出了以下幾點經(jīng)驗:
- 排查內(nèi)存泄漏:使用堆轉(zhuǎn)儲文件和分析工具,找出內(nèi)存泄漏的原因,并及時修復(fù)。
- 調(diào)整 JVM 參數(shù):根據(jù)系統(tǒng)的特點,調(diào)整堆內(nèi)存大小、年輕代大小、垃圾收集器等參數(shù)。
- 優(yōu)化數(shù)據(jù)庫查詢:加索引、分頁查詢、批量操作等,減少數(shù)據(jù)庫查詢的耗時和內(nèi)存占用。
- 優(yōu)化并發(fā)編程:調(diào)整線程池、復(fù)用對象、異步處理等,減少對象的創(chuàng)建和銷毀次數(shù)。
- 監(jiān)控 JVM 狀態(tài):使用監(jiān)控工具,實時監(jiān)控 JVM 的內(nèi)存使用情況、GC 頻率、線程狀態(tài)等指標(biāo),及時發(fā)現(xiàn)問題并解決。
FullGC 優(yōu)化是一個系統(tǒng)工程,需要從多個方面入手。只要你掌握了正確的方法,就能夠?qū)?FullGC 的頻率降到最低,讓系統(tǒng)運(yùn)行得更加穩(wěn)定和高效。
FullGC 并不可怕,可怕的是你不知道如何優(yōu)化它。只要你用心去學(xué)習(xí)和實踐,就一定能夠成為 FullGC 優(yōu)化的高手。