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

这篇帮到你了吗?

刚解决了一个棘手的报错?花两分钟记录下来,帮助下一个遇到同样问题的开发者。

贡献一条解法