OOM概述:什么是OutOfMemoryError,常见原因与分类
做Java开发这么多年,我遇到的第一个“硬核”报错就是OutOfMemoryError。那时候我还年轻,看到堆栈里一长串异常,第一反应是“服务器又挂了?”后来才知道,OOM是JVM在极度饥饿状态下发出的最后一声哀嚎。说白了,就是内存耗尽了,连新对象都分配不了。
为什么会这样?可能是堆太小,可能是代码有泄漏,也可能是某个数据结构膨胀到了失控的程度。今天我们就从根上捋一捋OOM的来龙去脉。
什么是OutOfMemoryError
OutOfMemoryError是Java的java.lang.OutOfMemoryError异常。它不同于普通的NullPointerException,它是一个严重错误,属于Error层级。一旦抛出,通常意味着JVM内存区域已经满了,而且垃圾回收也没能回收出可用空间。
你可能会问:JVM不是有GC吗?为什么还会内存不足?嗯,这里要注意:GC只能回收不再被引用的对象。如果对象一直被引用,或者引用的数量巨大,GC也是巧妇难为无米之炊。我有个项目曾经因为一个HashMap的key没重写equals方法,导致该放的放不进去,旧的对象永远无法被清除,最终堆空间爆了。
常见原因与分类
根据我的经验,OOM有五个最常见的“面孔”。我把它们整理成了下表,方便你对照排查:
| 错误消息 | 对应内存区域 | 典型原因 |
|---|---|---|
| Java heap space | 堆 | 对象泄漏、堆太小、一次性加载过多数据 |
| Metaspace | 元空间 | 类加载卸载不平衡、动态生成类过多 |
| Unable to create new native thread | 栈(线程) | 线程创建过多、操作系统线程限制 |
| Direct buffer memory | 直接内存 | NIO中ByteBuffer分配未释放 |
| GC overhead limit exceeded | 堆 | GC时间占比超过98%且回收效果极差 |
光是记住这些消息还不够,你得明白背后的机制。举个例子,「Java heap space」是最常见的。它表明堆空间满了。但为什么堆会满?可能是你用了ArrayList撑着几千万条记录没释放,也可能你循环里不断new一个大数组却不扔掉引用。我在一个报表系统里就见过,每次查询都把全表数据拉到内存里,用户一多,堆直接撑爆。
而「Metaspace」则跟类元数据有关。JDK8以后,永久代被移除,换成了元空间,使用的是本地内存。如果你频繁加载类(比如动态代理、CGlib),而又没做好清理,元空间就会涨到操作系统限制上限。我记得有次线上服务每分钟加载几百个临时类,元空间飙升到2GB,最后OOM。
还有一个很多人忽略的——「Unable to create new native thread」。这其实是操作系统层面线程资源耗尽。JVM的每个线程在OS里对应一个原生线程,当线程数超过上限(比如ulimit或pthread数量限制),就会报这个错。我曾经遇到过一个连接池配置失误,每来一个请求就创建新线程,没有复用,几分钟内线程数破千,服务器直接挂掉。
「Direct buffer memory」和「GC overhead limit exceeded」也各有各的坑。前者常见于Netty、Mina等NIO框架,直接内存不够就要看代码里有没有及时回收DirectBuffer。后者更诡异:JVM花了98%的时间做GC,只收回2%的内存,干脆罢工了。这时你看到日志里大量Full GC,但堆就是降不下来。
分类总结与整体脉络
为了更好地理解这些关系,我画了一张图:
这张图把五大OOM类型和常见的直接原因串联起来了。你看,大部分问题都源于资源管理失控。对于Java堆空间,往往是对象生命周期过长;对于元空间,则可能是类加载器没有回收。每个类型的排查方法略有不同,但核心思路一致:找到哪些对象在“吃”内存,以及为什么没被回收。
避坑指南与个人经验
-Xmx 搭配无限大的堆。我见过有人把堆设成机器物理内存的80%,结果系统其他进程没内存可用,直接OOM kill。堆的最大值应该基于压测结果和预留余量来确定。
我曾经花了两天时间排查一个诡异的OOM:服务每隔5小时准时挂一次,重启后又能撑5小时。后来抓Heap Dump发现,Tomcat的ClassLoader里存了一个静态的HashSet,每次请求都往里面加一条数据,但永远不remove。日积月累,内存就成了堰塞湖。你看,有时候OOM不是瞬间爆发的,而是慢慢“养”出来的。
再分享一个技巧:如果你遇到了「GC overhead limit exceeded」,别急着加堆内存。先检查是不是代码里有System.gc()调用,或者使用了某些老框架频繁触发Full GC。加堆有时反而延迟了问题的暴露,治标不治本。
- 读错误消息,确定内存区域(堆/元空间/栈/直接内存)
- 用jstat查看GC情况,确认GC是否正常
- 如果涉及堆,立即抓一份Heap Dump(jmap -dump:format=b,file=heap.hprof \
)
说到底,OOM不是洪水猛兽。只要你能理解JVM的内存模型,掌握基本的监控和Dump分析手段,大部分OOM都可在几分钟内定位。后面的章节,我会带大家一步步走通Heap Dump分析的完整流程,从生成到分析再到修复。嗯,今天就聊到这里,希望你心里对OOM已经有了一个清晰的轮廓。