頻繁Full GC如何優化?
前言
我們在面試時,經常會被面試官問到:線上服務頻繁Full GC該如何優化?
今天這篇文章跟大家一起聊聊這個話題,希望對你會有所幫助。
1. 什么是Full GC?
當老年代空間不足時,JVM會觸發Stop-The-World的全局回收(Full GC),暫停所有應用線程。
致命危害(生產環境實測):
暫停時間 | 業務影響 |
1秒 | 支付超時率上升5% |
3秒 | 數據庫連接池耗盡 |
10秒 | 服務被注冊中心摘除 |
對象的晉升之路流程圖:
圖片
關鍵代碼:年齡計數器
// HotSpot虛擬機源碼片段(objectMonitor.cpp)
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock) {
if (obj->age() >= MaxTenuringThreshold) { // 年齡閾值檢查
promote_to_old_gen(obj); // 晉升老年代
}
}2.如何排查定位問題?
2.1 實時監控:GC健康度速診
jstat -gcutil <pid> 1000 # 每秒輸出GC數據關鍵指標解讀:
- OU:老年代使用率 > 90% = 危險區
- FGCT:Full GC總耗時 > 應用運行時間10% = 嚴重問題
2.2. 堆內存轉儲:揪出內存黑洞
jmap -dump:live,format=b,file=heap.bin <pid> # 生產環境慎用live2.3 MAT深度分析:解剖內存泄漏
圖片
3.優化方案
方案1:對象池化——大對象的救贖
場景:高頻創建10MB的文件緩存
// 反例:每次請求創建新對象
public void processRequest(Request req) {
byte[] buffer = newbyte[10 * 1024 * 1024]; // 10MB
// ...處理邏輯
}
// 優化:對象池復用
privatestaticfinal ObjectPool<byte[]> pool = new GenericObjectPool<>(
new BasePooledObjectFactory<byte[]>() {
@Override
publicbyte[] create() {
returnnewbyte[10 * 1024 * 1024];
}
}
);
public void processRequest(Request req) throws Exception {
byte[] buffer = pool.borrowObject();
try {
// ...處理邏輯
} finally {
pool.returnObject(buffer);
}
}效果:老年代分配速率下降85%
方案2:手動控制晉升
問題:Survivor區過小導致對象提前晉升優化參數:
-XX:TargetSurvivorRatio=60 # Survivor區使用閾值
-XX:MaxTenuringThreshold=15 # 最大晉升年齡
-XX:+NeverTenure # 若Survivor足夠,永不晉升(慎用!)晉升原理:

方案3:合理分配堆空間
經典誤區:
-Xmx4g -Xms4g # 錯誤!未配置新生代優化公式:
新生代大小 = 總堆 * 3/8
Eden:Survivor = 8:1:1正確配置:
-Xmx8g -Xms8g
-Xmn3g # 新生代3G (8*3/8≈3)
-XX:SurvivorRatio=8 # Eden:Survivor=8:1:1方案4:卸載無用類
場景:熱部署頻繁的應用(如JRebel)診斷命令:
jcmd <pid> VM.class_stats # JDK8+
jcmd <pid> GC.class_stats # JDK11+根治代碼:
// 自定義類加載器必須實現close()
public class HotSwapClassLoader extends URLClassLoader {
@Override
public void close() throws IOException {
// 1. 停止新請求
// 2. 卸載所有類
// 3. 關閉資源
}
}方案5:顛覆傳統的ZGC
傳統GC痛點:
- CMS:內存碎片問題
- G1:Mixed GC不可控
ZGC遷移步驟:
- 升級JDK至17+
- 添加參數:
-XX:+UseZGC
-XX:ZAllocatinotallow=5.0 # 容忍內存分配速率波動
-Xmx16g -Xlog:gc*:file=gc.log效果對比:
指標 | CMS | ZGC |
Full GC次數 | 15次/天 | 0次/天 |
最大暫停 | 2.8秒 | 1.2毫秒 |
方案6:堆外內存治理
現象:堆內存正常,但Full GC頻繁根源:DirectByteBuffer的清理依賴Full GC防御方案:
// 方案1:限制堆外內存
-XX:MaxDirectMemorySize=512m
// 方案2:主動調用Cleaner
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
Cleaner cleaner = ((DirectBuffer) buffer).cleaner();
if (cleaner != null) cleaner.clean();
// 方案3:Netty的內存管理
PooledByteBufAllocator allocator = new PooledByteBufAllocator(true);
ByteBuf buffer = allocator.directBuffer(1024);
// ...使用后必須release!
buffer.release();4.實戰案例
背景:某支付系統日均交易10億癥狀:
- 每分鐘5次Full GC,暫停4.2秒
- 99線響應時間從50ms飆升至3秒
排查過程:
jstat顯示老年代10秒內從60%→99%- MAT分析發現
ConcurrentHashMap$Node[]占78%內存 - 溯源代碼找到緩存黑洞:
// 問題代碼:永不失效的緩存
Map<String, Transaction> cache = new ConcurrentHashMap<>();
public void cacheTransaction(Transaction tx) {
cache.put(tx.getId(), tx); // Key沖突時舊對象未移除!
}解決方案:
- 改用Caffeine緩存:
Cache<String, Transaction> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();- 添加ZGC參數
- 重寫線程池任務隊列:
// 用有界隊列替代LinkedBlockingQueue
new ThreadPoolExecutor(..., new ArrayBlockingQueue<>(1000));效果:
- Full GC降為0
- 99線回落至68ms
總結
- 監控三件套:
jstat -gcutil <pid> 1000 # 實時監控
-Xlog:gc*:file=gc.log # GC日志
Prometheus + Grafana # 可視化大盤- 參數黃金法則:
圖片
- 代碼軍規:
大對象必須池化
緩存必須設置上限
線程池必須用有界隊列
- GC算法選擇:
場景 | 推薦算法 |
堆<8G | Parallel |
8G~32G | G1 |
關鍵業務系統 | ZGC |
Full GC不是優化出來的,是設計出來的!
永遠在架構設計階段預留30%內存緩沖空間,比任何調參技巧都重要。
附錄:急救工具箱
工具 | 命令 | 適用場景 |
jcmd |
| 主動觸發Full GC |
Arthas |
| 內存快照 |
btrace | 監控DirectByteBuffer分配 | 堆外內存泄漏 |
PerfMa | 在線分析GC日志 | 自動化診斷 |




























