StackTraces
Java已驗證

Java OutOfMemoryError GC overhead limit exceeded

@debug_master32 天前更新閱讀 6 min00

它和 Java heap space 不是一回事:堆不一定真滿了,而是「GC 拼命幹、卻回收不出東西」。JVM 用這個錯誤做快速失敗,避免進程陷入「CPU 100% 跑 GC、業務毫秒變分鐘」的死循環。既然根子還是堆,就別先想著關掉這個檢查,先查漏不動。

Issue & Env(案發現場與環境)

  • 現象:應用運行緩慢,CPU 長時間打滿,最終拋 OOM,日誌裡帶 GC overhead limit exceeded。
  • 環境:Java 8+,使用任何 GC(Parallel / G1 等),常在堆偏小或存在記憶體洩漏時觸發。

StackTrace(堆疊日誌)

java.lang.OutOfMemoryError: GC overhead limit exceeded
    at java.base/java.util.HashMap.resize(HashMap.java:700)
    at java.base/java.util.HashMap.putVal(HashMap.java:658)
    at com.example.cache.InMemoryCache.put(InMemoryCache.java:93)
    at com.example.cache.InMemoryCache.load(InMemoryCache.java:65)
    at com.example.service.ProductService.getProduct(ProductService.java:41)
    at com.example.controller.ProductController.detail(ProductController.java:28)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:498)

注意堆疊頂部往往是普通的業務分配點,真正的信號是那句「GC overhead limit exceeded」本身。

Reproduce(復現程式碼)

一個無界緩存,不停往裡塞物件,物件還被強引用持有:

public class LeakyCache {
    static final Map<String, byte[]> CACHE = new HashMap<>();

    public static void main(String[] args) {
        for (int i = 0; ; i++) {
            CACHE.put("key" + i, new byte[1024 * 1024]); // 每人 1MB,强引用不释放
        }
    }
}

跑上一陣,堆被活物件填滿,GC 每次只能回收一點點,最終拋 GC overhead limit exceeded。

Root Cause(根因分析)

JVM 的觸發條件是:GC 花費的時間超過總運行時間的 98%,而且每次 GC 回收的堆不足 2%。滿足這兩條時,JVM 認為進程已經「近乎停擺」,乾脆拋錯快速失敗,而不是讓你卡著。

它和「記憶體真不夠」的區別在於成因:如果只是堆配小了、但物件都是短命的,GC 還能正常回收,你看到的是 Java heap space;而 GC overhead 通常是堆裡積壓了大量「長時間存活但最終無意義」的物件,每次 GC 掃一圈幾乎騰不出地方,只能反覆空轉。

典型誘因有:無界緩存、靜態集合不斷追加、事件監聽器 / 回調註冊後從不註銷、大物件被一個長生命週期集合抓著不放。所以它本質是洩漏或「活數據超過堆容量」的信號,而不是單純調大堆就能糊弄過去。

The Fix(解決方案)

先開堆轉儲,等下次 OOM 時拿到現場:

java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/dumps -jar app.jar

拿到 .hprof 後用 Eclipse MAT 打開,看 Leak Suspects 報告:如果是一個物件或集合主導了堆,那就是洩漏,回到程式碼把引用解開;如果是一大堆正常物件、堆確實裝不下,才考慮加大堆。

真需要加堆或換收集器:

# 加堆
java -Xms2g -Xmx4g -jar app.jar
# 大堆换 G1 / ZGC,减少停顿和开销
java -XX:+UseG1GC -Xmx4g -jar app.jar
java -XX:+UseZGC -Xmx4g -jar app.jar

臨時的 -XX:-UseGCOverheadLimit 只是關掉這個「快速失敗」的保護,讓進程繼續帶著近乎癱瘓的狀態跑。可以拿來臨時抓現場,但絕不能當長期方案——它不解決任何記憶體問題,只是讓你晚一點崩潰、更難看。

References(參考來源)

最後更新於 2026-09-09

這篇幫到你了嗎?

剛解決了一個棘手的報錯?花兩分鐘記錄下來,幫助下一個遇到同樣問題的開發者。

貢獻一條解法