前言
在 java 后端开发里,oom(outofmemoryerror) 绝对是线上最让人 “头皮发麻” 的问题之一。它不像普通异常那样好定位,往往服务跑着跑着突然崩掉,日志寥寥几句,让人无从下手。
这篇文章就把 oom 讲得通透、详细、能直接用于实战,从是什么、为什么、有哪些类型、怎么排查、怎么根治,一次性讲全。
一、什么是 oom?
oom 全称 outofmemoryerror,意思是:jvm 无法为新对象分配内存,且垃圾回收也无法腾出足够空间,最终抛出的致命错误。
重点理解三句话:
- oom 是 error,不是 exception,一旦出现,应用基本不可用。
- 不是 “内存不够”,而是 内存被占满、且回收不了。
- 90% 的 oom 不是 jvm 内存太小,而是 代码问题。
二、oom 出现的根本原因
一句话总结:程序不断创建对象 → 对象一直被引用 → gc 无法回收 → 内存被撑爆 → 新对象无处安放 → oom
常见根源:
- 内存泄漏:对象不用了,但一直被持有,gc 无法回收
- 一次性加载过多数据:不分页查全表、大文件全量读入内存
- 资源未关闭:连接、流、线程池滥用
- 静态集合无限添加对象
- jvm 参数不合理:堆、元空间设置过小
- 动态生成大量类:导致元空间溢出
三、jvm 内存区域与 oom 的关系
java 虚拟机把内存分成不同区域,每个区域都可能 oom:
- heap 堆:存放对象实例 → 最常见 oom
- metaspace 元空间:存放类信息、方法、常量、代理类
- 虚拟机栈:方法调用栈 → 栈溢出
- 本地方法栈:native 方法使用
- 直接内存:nio 使用的堆外内存
- 程序计数器:唯一不会 oom 的区域
下面我们逐个讲最常出现的 6 种 oom。
四、6 种最经典 oom 场景详解
1. 堆内存溢出(java heap space)
错误信息:
java.lang.outofmemoryerror: java heap space
原因:堆内存被占满,不断创建对象,且都被强引用,无法 gc。
典型代码:
list<object> list = new arraylist<>();
while (true) {
list.add(new object());
}
真实业务场景:
- 不分页查询数据库全表数据
- 大文件一次性全部加载到内存
- 死循环创建对象
- 静态集合无限添加数据
这是 线上最常见的 oom。
2. 元空间溢出(metaspace)
错误信息:
java.lang.outofmemoryerror: metaspace
原因:加载的 类太多,元空间放不下。
常见场景:
- 大量动态代理(cglib、mybatis、spring 代理)
- 反射频繁生成类
- 热部署过多
- 自定义类加载器未正确释放
jvm 参数:
-xx:metaspacesize=128m -xx:maxmetaspacesize=256m
3. 直接内存溢出(direct buffer memory)
错误信息:
java.lang.outofmemoryerror: direct buffer memory
原因:nio 使用 bytebuffer.allocatedirect() 申请的堆外内存耗尽。
常见场景:
- netty、mina 等 nio 框架
- 大文件上传下载、流媒体处理
4. 栈溢出(stackoverflowerror)
错误信息:
java.lang.stackoverflowerror
原因:方法调用层级太深,栈帧数量超过栈最大深度。
典型代码:
public void loop() {
loop(); // 无限递归
}
业务场景:
- 递归没有出口
- 循环调用 a→b→a→b
5. 无法创建本地线程
错误信息:
java.lang.outofmemoryerror: unable to create new native thread
原因:每个线程都占用栈内存,线程数超出系统限制。
场景:
- 代码里随便
new thread() - 线程池参数不合理,maximumpoolsize 过大
- 高并发下无节制创建线程
6. gc 开销超限
错误信息:
java.lang.outofmemoryerror: gc overhead limit exceeded
原因:jvm 98% 的时间在做 gc,却只回收不到 2% 内存,进入 “累死状态”。
本质:内存快空了,又不断产生垃圾。
五、内存泄漏 vs 内存溢出
这是面试 + 实战必考点。
1. 内存溢出(oom)
- 内存真的不够用了
- 一次性加载太多对象
- 加内存可以临时缓解
2. 内存泄漏(memory leak)
- 对象不用了,但一直被引用,gc 无法回收
- 内存慢慢被吃掉
- 最终一定会导致 oom
- 加内存没用,必须改代码
最常见泄漏场景:
- static list/map 不断添加对象
- threadlocal 没
remove() - 数据库连接、io 流未关闭
- 内部类持有外部类引用
- 缓存不加过期、淘汰机制
六、线上 oom 标准排查流程
第一步:必须开启 oom 自动 dump(重中之重)
在 jvm 启动参数里加:
-xx:+heapdumponoutofmemoryerror -xx:heapdumppath=/data/logs/heap.hprof
oom 时会自动导出内存快照,这是定位问题的关键。
第二步:分析 dump 文件
工具:
- eclipse mat(最强大)
- jprofiler
- arthas(阿里开源,线上神器)
重点看 4 点:
- 哪个类的对象最多
- 谁在引用这些对象
- 是不是业务对象(是 → 代码问题)
- gc roots 链在哪里
第三步:配合命令行工具定位
jstat -gc 进程id 1000 10 # 看gc情况 jmap -heap 进程id # 看堆配置 jmap -histo 进程id # 看对象数量 arthas-boot.jar # 阿里arthas,一键排查
判断内存泄漏最简单方式:每次 full gc 后,内存水位不下降 → 100% 内存泄漏
七、oom 通用解决方案
1. 代码层面(根治)
- 数据库查询必须分页
- 大文件使用流式读取,不一次性加载
- 资源用完必须关闭(连接、流、threadlocal)
- 慎用 static 集合
- 线程池标准化,不无限创建线程
- 缓存加过期、淘汰、降级策略
2. jvm 参数优化
-xms2g -xmx2g # 堆初始值和最大值 -xx:metaspacesize=256m -xx:maxmetaspacesize=256m -xx:+heapdumponoutofmemoryerror
3. 架构层面
- 消息队列削峰
- 分布式计算,压力分散
- 大文件上传走分片
- 热点数据放缓存,不全部放内存
八、总结(一句话记住 oom)
- oom = 内存满了,新对象没地方分配
- java heap space 最常见,大多是没分页、内存泄漏
- metaspace 溢出 = 类太多
- direct buffer 溢出 = nio/netty 问题
- stackoverflow = 递归 / 调用太深
- 无法创建线程 = 线程太多
- 排查核心:dump 文件 + mat/arthas
- 90% oom 都是代码问题,不是 jvm 内存太小
到此这篇关于java oom异常从原理、场景、排查到解决方案的文章就介绍到这了,更多相关java oom异常原理、场景、排查内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论