Tomcat7+OpenJDK1.8.0_162触发OOM后堆dump分析无异常求助
搞定GC Overhead Limit Exceeded:堆转储有空但还是炸了?
兄弟,我太懂这种抓瞎的感觉了——明明堆转储显示大把空闲内存,却还是触发了GC开销超限的异常,简直离谱!咱们先把核心逻辑理清楚,再一步步排查:
首先得掰明白一个关键点:java.lang.OutOfMemoryError: GC overhead limit exceeded不是因为堆内存真的满了,而是JVM在短时间内花了98%以上的CPU时间做GC,但每次只回收了不到2%的内存,它觉得再GC下去也是浪费资源,干脆直接抛异常。而你拿到的堆转储之所以有空,大概率是因为JVM抛异常前会强制跑一次Full GC,转储的是GC后的状态——相当于你刚把房间收拾干净,才拍照片给别人看,根本看不到乱成狗的那个瞬间!
接下来给你几个实打实的排查方向:
1. 先把堆转储的时机改对,抓准“案发瞬间”
默认配置下,JVM是先GC再转储,等于把关键证据销毁了。你改一下JVM启动参数,让它在Full GC之前就生成堆转储:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/tomcat7/ -XX:-HeapDumpAfterFullGC -XX:+HeapDumpBeforeFullGC
下次再炸的时候,拿到的就是触发异常前的内存快照,这下就能看到到底是哪些对象在搞事情了。
2. 排查那些“看不见”的内存消耗
有时候堆转储看着空,实则藏着坑:
- 软/弱引用堆成山:这些对象虽然会被GC回收,但如果数量太多,JVM每次GC都要花大量时间扫描清理,时间占比一超标就触发异常。你在MAT里搜一下
Soft Reference和Weak Reference的数量,要是一堆堆的,就得查代码里是不是有地方滥用了这些引用类型。 - 内存碎片搞事情:老年代里碎片太多,总内存够,但没有连续的大块空间给大对象分配,JVM就只能反复GC试图整理空间。你可以在MAT里看老年代的内存分布,或者加个
-XX:+PrintHeapAtGC参数,看GC前后的堆内存细节,确认是不是碎片问题。
3. 从日志里找触发异常的“真凶”
堆转储不给力,就去扒其他日志:
- 翻Tomcat的
access.log,看异常时间点前后有没有突发的大量请求,或者某个接口被疯狂调用——比如批量导出、大文件上传这类操作,短时间内会创建一堆临时对象,直接把GC干懵。 - 开GC日志,详细记录GC的频率、耗时和回收量:
重点看异常前的GC记录:是不是连续好几次Full GC,每次回收的内存少得可怜,同时年轻代GC频率高到离谱?这说明要么年轻代设太小,对象直接进老年代;要么有对象疯狂创建又快速变老。-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:/var/log/tomcat7/gc.log
4. 调优JVM参数,适配你的应用
针对Tomcat7+OpenJDK1.8,这几个参数可以重点检查:
- 年轻代大小:如果
-Xmn设得太小,对象很快就会挤到老年代,导致老年代频繁Full GC。可以试着调到堆内存的1/3到1/2,比如堆内存给了4G,年轻代就设1.5G左右。 - 换个GC收集器:1.8默认是Parallel GC,适合后台计算,但Web应用更适合用CMS或者G1 GC(1.8u40之后G1支持很稳),它们的停顿时间更短,不容易触发开销限制。比如换G1的配置:
-XX:+UseG1GC -XX:MaxGCPauseMillis=200
5. 代码里藏的坑也不能放过
- 查
ThreadLocal有没有没清理的:线程池里的线程会一直活着,如果ThreadLocal绑定的对象没remove,这些对象就会一直占着内存,GC也收不走。 - 查缓存逻辑:有没有缓存没设过期时间,或者缓存了超大对象,导致内存越积越多?
- 查批量操作:是不是一次性从数据库捞了几万条数据到内存,或者循环里创建了大量字符串、集合对象,用完也没及时释放?
内容的提问来源于stack exchange,提问作者Kuba
相关产品推荐
相关产品推荐

