一、內存問題的三大根源
在動手優化前,先明確問題來源:
|
類型
|
表現
|
根本原因
|
|
1. 內存泄漏(Memory Leak) |
堆使用持續增長,GC 無法回收
|
對象被意外長期持有(如靜態集合、未註銷監聽器)
|
|
2. 內存浪費(Inefficiency) |
對象創建過多、結構冗餘
|
過度封裝、大對象、重複數據
|
|
3. 資源配置不當 |
容器 OOMKilled、頻繁 GC
|
堆太小、未限制非堆、GC 策略錯誤
|
✅ 優化目標:
- 消除泄漏
- 減少不必要的內存佔用
- 合理分配 JVM 內存區域
二、第一步:預防 —— 編寫“內存友好”的代碼
1. 避免靜態集合類無限增長
// ❌ 危險!緩存永不清理
private static Map<String, Object> cache = new HashMap<>();
// ✅ 使用帶淘汰策略的緩存
private static Cache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(10))
.build();
2. 及時釋放資源
- 使用
try-with-resources管理流、連接、文件句柄 - 註銷事件監聽器、定時任務、回調函數
3. 減少臨時對象創建
// ❌ 每次循環都 new
for (int i = 0; i < list.size(); i++) {
String key = "prefix_" + i; // 創建新 String
}
// ✅ 複用 StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < list.size(); i++) {
sb.setLength(7); // reset
sb.append("prefix_").append(i);
}
4. 謹慎使用大對象
- 單個對象 > 1MB 可能直接進入老年代(G1 中為 Humongous Region)
- 拆分大數組、使用流式處理(如
Stream+limit())
5. 選擇高效數據結構
|
場景
|
推薦
|
避免
|
|
小量鍵值對
|
|
|
|
布爾標誌
|
|
|
|
字符串拼接
|
|
|
三、第二步:分析 —— 定位內存瓶頸
1. 開啓基礎監控
# 必開參數
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dumps/
-Xlog:gc*:file=/logs/gc.log
2. 實時查看內存分佈
# 查看堆內存各代使用
jstat -gc <pid> 5s
# 查看 Metaspace
jstat -gcmetacapacity <pid>
3. 生成堆轉儲(Heap Dump)分析
# 手動生成 dump
jmap -dump:live,format=b,file=heap.hprof <pid>
使用工具分析:
- Eclipse MAT(Memory Analyzer Tool):
- 查看 Dominator Tree(誰佔內存最多)
- 檢查 Leak Suspects Report(自動識別泄漏)
- VisualVM / JProfiler:
- 實時監控對象創建速率
- 追蹤對象分配棧(Allocation Call Tree)
🔍 關鍵技巧:
在 MAT 中搜索java.util.HashMap、ArrayList、ThreadLocal、ClassLoader,這些是泄漏高發區。
四、第三步:優化 —— 針對性調優
場景 1:老年代持續增長 → 可能內存泄漏
- 對策:
- 用 MAT 分析
retained heap最大的對象 - 檢查是否被靜態變量、緩存、線程池引用
- 修復代碼:改用弱引用(
WeakHashMap)、加 TTL、手動清理
場景 2:Young GC 頻繁 → 新生代壓力大
- 對策:
-Xmn3g # 增大新生代(假設堆 6GB)
-XX:SurvivorRatio=8 # Eden:S0:S1 = 8:1:1,增大 Eden
- 同時優化代碼:減少短命對象創建
場景 3:Metaspace OOM
- 對策:
-XX:MaxMetaspaceSize=256m # 必須設上限!
- 檢查是否動態生成類過多(Spring AOP、Groovy、反射)
場景 4:容器 OOMKilled(RSS 超限)
- 對策:
-XX:+UseContainerSupport
-XX:MaxRAMPercentage=70.0
-XX:MaxMetaspaceSize=128m
-XX:MaxDirectMemorySize=128m
- 用
jcmd <pid> VM.native_memory summary檢查 Native 內存
五、第四步:驗證 —— 用數據證明優化有效
1. 壓測對比
- 使用 JMeter / wrk 模擬高峯流量
- 對比優化前後:
- 堆內存使用曲線(是否平穩?)
- GC 頻率與停頓時間
- RSS 是否 < 容器 limit × 95%
2. 監控指標
|
指標
|
健康閾值
|
|
堆使用率
|
< 70%,無持續上升趨勢
|
|
Young GC
|
< 1 次/秒(Web 服務)
|
|
Full GC
|
0 次/天
|
|
Metaspace
|
< 80% of Max
|
|
RSS / 容器 limit
|
< 0.95
|
3. 自動化迴歸
- 將內存使用納入 CI/CD 質量門禁
- 例如:部署後自動檢查
jstat輸出,若 OU(老年代使用)連續 10 分鐘增長 > 5%,則告警
六、高級技巧(進階)
1. 啓用字符串去重(G1/ZGC)
-XX:+UseStringDeduplication
- 自動合併內容相同的
String對象(需 G1 或 ZGC) - 可節省 10%~20% 堆內存(尤其日誌、JSON 場景)
2. 壓縮普通對象指針(CompressedOops)
- JDK 8+ 默認開啓(堆 < 32GB 時)
- 減少對象頭和引用佔用(64 位指針 → 32 位)
3. 使用對象池(謹慎!)
- 僅適用於創建成本極高的對象(如
ThreadLocalRandom、DateTimeFormatter) - 避免通用對象池(如 Apache Commons Pool),可能引發新泄漏
4. Native Memory Tracking(NMT)
-XX:NativeMemoryTracking=summary
jcmd <pid> VM.native_memory summary
- 分析 JVM 本體、Metaspace、線程、Direct Buffer 的 native 內存使用