首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 常見記憶體溢出 (OOM) 異常深度剖析與解決方案

     編輯:Java 相關知識

JVM 記憶體結構與 OOM 分類

HotSpot JVM 將堆劃分為:新生代、老年代、永久代/元空間。不同區域滿載會拋出不同的 OOM 異常。

五大常見 OOM 類型與解決方案

1. Java Heap Space (堆記憶體不足)

java.lang.OutOfMemoryError: Java heap space

原因分析

  • 物件建立過快、釋放過慢 (記憶體洩漏)
  • 單次載入過大資料集 (如一次查詢百萬筆資料)
  • 堆設定過小 (-Xmx 過小)
  • 大物件直接進入老年代 (超過 -XX:PretenureSizeThreshold)

排查步驟

  1. 啟動參數加入堆傾印:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump.hprof
  2. 使用 MAT (Memory Analyzer Tool) / JVisualVM / Arthas 分析 dump
  3. 找出 GC Roots 到佔用記憶體最大物件的引用鏈
  4. 判斷是 記憶體洩漏 (引用未釋放) 還是 記憶體溢出 (確實需要更多記憶體)

解決方案

  • 調大堆:-Xms2g -Xmx2g (建議 Xms=Xmx 避免震蕩)
  • 修復洩漏:釋放集合引用、關閉資源、修正單例模式濫用、ThreadLocal 清理
  • 優化演算法:分頁查詢、流式處理、避免大集合長期持有
  • 調整 GC 策略:G1/ZGC 適用大堆低延遲場景

2. PermGen Space / Metaspace (元空間不足)

# Java 7 永久代
java.lang.OutOfMemoryError: PermGen space

# Java 8+ 元空間
java.lang.OutOfMemoryError: Metaspace

原因分析

  • 類載入過多:動態生成類 (CGLIB、JSP、JDK Proxy、Groovy、JSP 編譯)
  • 熱部署/重啟未卸載類 (Tomcat 熱部署類洩漏經典案例)
  • 字串常量池過大 (String.intern() 濫用)
  • 元空間預設無上限,但受限於本機記憶體

解決方案

  • Java 7:-XX:PermSize=256m -XX:MaxPermSize=512m
  • Java 8+:-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
  • 減少動態生成類、避免 String.intern() 大量使用
  • Tomcat 熱部署問題:升級容器、使用 JRebel、生產環境避免頻繁熱部署

3. Unable to Create Native Thread (無法建立原生執行緒)

java.lang.OutOfMemoryError: unable to create new native thread

原因分析

執行緒建立需要本機記憶體 (非堆),公式:

可用本機記憶體 = 進程最大記憶體 - (-Xmx) - (-XX:MaxMetaspaceSize) - 程式計數器 - 其他
  • -Xmx 設定過大,擠佔本機記憶體
  • -Xss (執行緒棧大小) 設定過大 (預設 1MB)
  • 程式建立過多執行緒 (未使用執行緒池、執行緒洩漏)
  • 作業系統限制 (ulimit -u、/proc/sys/kernel/pid_max)

解決方案

  • 使用執行緒池:Executors.newFixedThreadPool() 限制並發數
  • 減小堆/元空間:騰出本機記憶體給執行緒棧
  • 減小執行緒棧:-Xss256k (需測試棧溢出風險)
  • 調整 OS 限制:ulimit -u 65535

4. StackOverflowError (棧溢出)

java.lang.StackOverflowError

原因分析

  • 遞迴深度過深、無終止條件
  • 大量區域變數、方法參數導致棧幀過大
  • 循環呼叫 (A→B→A)

解決方案

  • 改寫為迴圈/疊代算法
  • 增加棧大小:-Xss2m (治標不治本)
  • 尾遞迴優化 (JVM 不支援,需手動轉換)

5. Direct Buffer Memory (直接記憶體不足)

java.lang.OutOfMemoryError: Direct buffer memory

原因分析

  • NIO ByteBuffer.allocateDirect() 大量分配未釋放
  • Netty、RocketMQ、Druid 等框架大量使用直接記憶體
  • 預設大小受 -XX:MaxDirectMemorySize 限制 (預設等於 -Xmx)

解決方案

  • 調大:-XX:MaxDirectMemorySize=2g
  • 確保釋放:Cleaner.clean() 或依賴 GC (較慢)
  • 檢查框架配置 (Netty -Dio.netty.maxDirectMemory)

實戰排查工具鏈

工具用途適用階段
jstat -gcutilGC 頻率、各區使用率線上快速觀測
jmap -histo:live存活物件統計定位大物件類型
jmap -dump生成堆快照離線深度分析
MAT (Eclipse Memory Analyzer)支配樹、洩漏報告、OQL 查詢堆傾印深度分析 (首選)
JVisualVM / JMC視覺化監控、取樣分析開發測試階段
Arthas / Greys線上診斷、類載入統計、執行緒狀態生產環境零侵入診斷
GC 日誌分析 (GCeasy, gceasy.io)GC 停頓、吞吐量、擴容/縮容趨勢GC 調優

MAT 核心分析步驟 (圖文並茂)

  1. 開啟 .hprof → Leak Suspects Report 自動分析洩漏嫌疑
  2. Histogram → 按 Retained Heap 排序 → 找出佔用最大的類別
  3. 右鍵 → List Objects → with outgoing references → 追蹤 GC Roots 引用鏈
  4. 找到 累積點 (如 static 集合、ThreadLocal、單例緩存) → 定位代碼位置

經典洩漏模式與修復

1. 靜態集合持有參考

// 錯誤
static List<User> userCache = new ArrayList<>();
userCache.add(user); // 永不清理

// 修復
static Map<String, User> userCache = new ConcurrentHashMap<>();
userCache.put(key, user);
// 定時清理或用弱引用
static Map<String, WeakReference<User>> weakCache = new ConcurrentHashMap<>();

2. ThreadLocal 未清理

// 錯誤
ThreadLocal<Connection> tl = new ThreadLocal<>();
tl.set(conn); // 執行緒池複用時不會自動清理

// 修復
try {
    tl.set(conn);
    // 業務邏輯
} finally {
    tl.remove(); // 必須在 finally 中清理
}

3. 內部類持有外部類引用

// 錯誤:非靜態內部類隱含持有外部類 this 參考
class Outer {
    byte[] bigData = new byte[10*1024*1024];
    class Inner {} // 持有 Outer 參考
    Inner createInner() { return new Inner(); }
}

// 修復:靜態內部類或傳遞所需資料而非 this
static class Inner {}

4. 連接/流/資源未關閉

// 修復:try-with-resources (Java 7+)
try (Connection conn = ds.getConnection();
     PreparedStatement ps = conn.prepareStatement(sql);
     ResultSet rs = ps.executeQuery()) {
    // 使用資源
} // 自動關閉

JVM 參數調優速查表

# 基礎堆設定 (建議 Xms=Xmx)
-Xms4g -Xmx4g

# 新生代/老年代比例
-Xmn1g              # 新生代 1GB
-XX:NewRatio=2      # 老年代:新生代 = 2:1

# 元空間
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m

# 直接記憶體
-XX:MaxDirectMemorySize=1g

# 執行緒棧
-Xss512k

# GC 選擇 (JDK 11+)
-XX:+UseG1GC                    # G1 (大堆、低延遲)
-XX:+UseZGC                     # ZGC (超大堆、亞毫秒延遲)
-XX:+UseShenandoahGC            # Shenandoah

# GC 日誌 (生產必開)
-Xlog:gc*:file=/logs/gc.log:time,uptime,level,tags:filecount=10,filesize=100m

# OOM 傾印
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/dumps/heap_%p_%t.hprof

# 類卸載 (解決 PermGen/Metaspace 洩漏)
-XX:+CMSClassUnloadingEnabled (CMS)
-XX:+ClassUnloadingWithConcurrentMark (G1)

總結:OOM 排查黃金三步曲

  1. 識別類型:看異常訊息判斷是 Heap、Metaspace、Thread、Direct Buffer、Stack
  2. 獲取證據:開啟 HeapDump、GC 日誌、用 Arthas/jmap 導出快照
  3. 定位根因:MAT 分析支配樹 → 找 GC Roots 引用鏈 → 定位代碼 → 修復洩漏或調優參數

核心原則:參數調優是治標,代碼修復是治本。大多數 OOM 根源於程式碼的記憶體洩漏或不合理的資料處理邏輯,而非單純記憶體不足。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved