java.lang.outofmemoryerror: gc overhead limit exceeded表示jvm在98%以上时间执行gc却仅回收不足2%堆内存,连续5次后主动抛出该错误,是慢性内存泄漏或堆配置不当的典型症状,非死锁直接表现。

这个错误本身不直接表明系统死锁,也不会在 GC 日志里写“死锁”二字。它反映的是一个持续性的内存回收失衡状态:JVM 在反复做 GC,但每次收效极差——典型表现是:
- GC 累计耗时 > 98% 的应用运行时间
- 每次 GC 仅回收
- 这种低效循环连续发生多次(默认 5 次)后,JVM 主动抛出
GC overhead limit exceeded
所以,看到这个报错,你要做的是顺着 GC 日志往下挖,而不是直接断定“死锁了”。
如何从 GC 日志识别慢性内存泄漏
GC 日志是你判断泄漏节奏的关键证据。启用方式(推荐):
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/to/gc.log -XX:+UseGCOverheadLimit
重点关注以下模式:
-
新生代 GC(Young GC)越来越频繁,但每次回收量却变少
→ 表明对象“活”得比预期久,可能被老年代或静态引用意外持有了 -
Full GC 频率上升,但老年代释放量微乎其微(如只降几 MB)
→ 强烈提示老年代中存在长期存活、无法回收的对象集合 -
GC 后堆内存使用率持续走高,且不回落(例如:
[PSYoungGen: 1200M->150M(1536M)] [ParOldGen: 3800M->3798M(4096M)])
→ 老年代几乎不释放,新生代回收后又快速填满 → 典型慢性泄漏特征
✅ 小技巧:用
gclogviewer或gceasy.io上传日志,它会自动标出“Memory Leak Suspected”段落和增长斜率异常的内存区域。
死锁和 GC overhead 是两回事,但可能共存
GC 日志不会显示线程死锁。如果你怀疑死锁导致对象堆积(比如线程卡在数据库连接获取、同步块里,迟迟不释放资源),需另查:
- 执行
jstack <pid></pid>,搜索deadlock或人工检查BLOCKED/WAITING线程是否形成环形等待 - 查看线程 dump 中是否有大量线程停在相同方法(如
synchronized (Cache.class)、DataSource.getConnection()) - 结合 GC 日志中 Full GC 时间突增(比如某次耗时 8 秒),再对应时间点查
jstack,看是否线程大面积阻塞
⚠️ 注意:死锁一般导致请求卡住、线程数飙升、CPU 不高但响应全无;而 GC overhead 通常伴随 CPU 持续 80%+、应用假死但线程活跃、日志刷屏式 GC 记录。
判定慢性泄漏的三个关键动作
比对多时段堆快照
用jmap -dump:format=b,file=heap.hprof <pid></pid>在不同时间(如间隔 10 分钟)抓两次 dump,用 MAT 对比 Dominator Tree,看哪些类实例数/总大小稳定增长(如HashMap$Node、char[]、ArrayList)-
检查静态持有、监听器、缓存未清理
常见泄漏源:-
static Map<string object></string>不设上限也不淘汰 - 注册了
ServletContextListener或ThreadLocal但没 remove - 数据库连接、流、NIO Buffer 未 close
-
观察对象生命周期是否违背业务逻辑
例如:一个订单处理方法本该瞬时完成,结果生成的OrderContext对象在 heap 中存活超 30 分钟 → 很可能被某个全局队列、异步回调或日志上下文意外强引用
基本上就这些。GC overhead 是个“症状报警”,不是诊断结论。它告诉你:内存正在无声流失,或者设计上就没打算还。真正要动手的,是日志 + dump + 代码三者交叉验证。










