一、內存問題的三大根源

在動手優化前,先明確問題來源:

類型

表現

根本原因

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. 選擇高效數據結構

場景

推薦

避免

小量鍵值對

ArrayMap(Android)或 ImmutableMap

HashMap(overhead 高)

布爾標誌

BitSet

boolean[]List<Boolean>

字符串拼接

StringBuilder

String +(編譯器不總能優化)


三、第二步:分析 —— 定位內存瓶頸

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.HashMapArrayListThreadLocalClassLoader,這些是泄漏高發區。


四、第三步:優化 —— 針對性調優

場景 1:老年代持續增長 → 可能內存泄漏

  • 對策
  1. 用 MAT 分析 retained heap 最大的對象
  2. 檢查是否被靜態變量、緩存、線程池引用
  3. 修復代碼:改用弱引用(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. 使用對象池(謹慎!)

  • 僅適用於創建成本極高的對象(如 ThreadLocalRandomDateTimeFormatter
  • 避免通用對象池(如 Apache Commons Pool),可能引發新泄漏

4. Native Memory Tracking(NMT)

-XX:NativeMemoryTracking=summary
jcmd <pid> VM.native_memory summary
  • 分析 JVM 本體、Metaspace、線程、Direct Buffer 的 native 內存使用