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