GC overhead limit exceeded 本质是 JVM 的“疲劳预警”,表明 GC 花费超98%时间却仅回收≤2%内存,反映对象生命周期失控而非单纯内存不足。

为什么 GC overhead limit exceeded 不是内存真不够用
这个错误本质是 JVM 的“疲劳预警”:不是堆内存彻底耗尽,而是 GC 花了太多时间(默认 98% 以上)却只回收了极少内存(默认 ≤2%),说明对象在老年代堆积、频繁晋升、或存在隐式强引用导致无法释放。它常出现在数据批量处理、缓存未清理、监听器未注销等场景,而非单纯调大 -Xmx 就能解决。
- 默认阈值是可调的:
-XX:GCTimeRatio=99(即允许 1% 时间做有用工作)、-XX:GCHeapFreeLimit=1(回收后至少剩 1% 空闲)——但掩盖问题比定位问题更危险 - 开启
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps后,如果日志里反复出现Full GC且每次只回收几 MB,基本可断定是对象生命周期管理失控 - JDK 8u40+ 默认启用该限制;JDK 11+ 仍保留,但部分 GC(如 ZGC、Shenandoah)因设计不同,几乎不会触发此错误
怎么快速定位谁在制造“GC 垃圾”
别先改参数,先抓现场。重点看三类对象:缓存未过期、静态集合持续 add、线程局部变量(ThreadLocal)未 remove。用 jstat 和 jmap 组合比看 GC 日志更快。
- 执行
jstat -gc <pid> 2000,观察OU(老年代使用量)是否单向增长、OC(老年代容量)是否稳定——若OU/OC持续 >90%,说明对象没被回收,不是 GC 不给力,是代码没放手 - 紧急 dump:
jmap -dump:format=b,file=heap.hprof <pid>,用VisualVM或Eclipse MAT打开,按dominator_tree排序,重点关注HashMap、ArrayList、ConcurrentHashMap的实例数和 retained heap - 特别注意匿名内部类持有外部类引用的情况:比如在
Runnable里直接引用了大对象,又把它塞进线程池队列,队列不消费就一直卡着
WeakReference 和 SoftReference 不是万能解药
很多人一看到内存问题就加 WeakReference,结果发现根本没效果。因为它们的回收时机受 GC 类型和 JVM 实现影响极大,且不能替代正确的生命周期控制。
-
WeakReference在下一次 GC(包括 Minor GC)时就可能被清掉,适合临时缓存,但不适合“可能还要用”的场景;SoftReference更“软”,但 JDK 8+ 默认策略是按内存压力延迟回收,压力不大时它比强引用还顽固 - 用
WeakHashMap存监听器?必须确保 key 是弱可达的——如果外面还有强引用指向同一个 key 对象,value 还是不会被释放 - 最稳妥的方式仍是显式清理:
cache.remove(key)、listenerList.clear()、threadLocal.remove(),而不是赌 GC
哪些 JVM 参数调整真有用,哪些只是拖延时间
调参前请确认你已看过堆转储。盲目加 -XX:-UseGCOverheadLimit 是最差选择——它只是关掉警报,不解决任何问题。
立即学习“Java免费学习笔记(深入)”;
- 真正有效的调整:
-XX:+UseG1GC(G1 对大堆和低延迟更可控)、-XX:MaxGCPauseMillis=200(让 G1 主动压缩,减少碎片)、-XX:G1HeapRegionSize=1M(避免大对象直接进老年代) - 对 CMS 用户:
-XX:CMSInitiatingOccupancyFraction=70可提前触发并发收集,但 JDK 9+ 已废弃 CMS,别再深陷 - 永远不要只调
-Xmx:如果-Xms和-Xmx差距过大,JVM 会频繁扩容堆,触发额外 GC;建议设为相等,让 GC 行为更可预测
真正难的从来不是调哪个参数,而是找到那个忘了 close() 的流、那个没 unregister 的事件监听器、那个用 static final Map 缓存了全量数据库结果的类——这些地方不会报错,只会慢慢把 GC 拖垮。











