6.1 一次 OOM 的内存布局透视 本节摘要:OOM 不是一种故障,是一族。本节从一次订单导出的堆 OOM 讲起,画出运行时数据区全景,逐个对应 OOM 报错与故障区域(堆、元空间、栈、直接内存、Finalizer),讲清堆的分代结构与大对象规则,最后走一遍从 GC 日志到 MAT 的完整排查动作。 事故现场:一次订单导出拖垮整个服务 运营在管理后台点了"导出全年订单",十几分钟后服务雪崩。日志里: 恢复后复盘代码:导出接口把 300 万行订单一次性查进内存拼 Excel,一行按 2KB 算就是 6GB,4GB 的堆毫无机会。更糟的是这发生在与在线交易共享的实例上——一次后台操作放倒了整个交易链路。
本节摘要:OOM 不是一种故障,是一族。本节从一次订单导出的堆 OOM 讲起,画出运行时数据区全景,逐个对应 OOM 报错与故障区域(堆、元空间、栈、直接内存、Finalizer),讲清堆的分代结构与大对象规则,最后走一遍从 GC 日志到 MAT 的完整排查动作。
运营在管理后台点了"导出全年订单",十几分钟后服务雪崩。日志里:
java.lang.OutOfMemoryError: Java heap space
恢复后复盘代码:导出接口把 300 万行订单一次性查进内存拼 Excel,一行按 2KB 算就是 6GB,4GB 的堆毫无机会。更糟的是这发生在与在线交易共享的实例上——一次后台操作放倒了整个交易链路。这是最典型的堆 OOM:容量需求超过了堆上限,根因是代码结构(整载)而非参数(堆太小),调大堆只是把崩溃推迟到更大的导出。
OOM 报错信息的后半句就是地名,按图索骥:
// 堆 对象实例 数组 默认物理上限受 Xmx 约束 java.lang.OutOfMemoryError: Java heap space // 堆内 GC 开销兜底 98 时间回收 2 内存 java.lang.OutOfMemoryError: GC overhead limit exceeded // 元空间 存类元数据 本地内存 默认无上限但受物理内存约束 java.lang.OutOfMemoryError: Metaspace // 线程栈 每线程一块 java.lang.OutOfMemoryError: unable to create new native thread // 直接内存 NIO Buffer 本地分配 java.lang.OutOfMemoryError: Direct buffer memory
各区域的参数与典型诱因:
| 区域 | 关键参数 | 典型事故诱因 |
|---|---|---|
| 堆 | -Xms -Xmx | 大结果集整载、无界缓存、内存泄漏 |
| 元空间 | -XX:MaxMetaspaceSize | 动态类生成(CGLib 代理、脚本引擎)、类加载泄漏 |
| 线程栈 | -Xss | 线程数 × 栈大小超进程内存(第 4 章 BIO 天花板) |
| 直接内存 | -XX:MaxDirectMemorySize | NIO 缓冲滥用、未释放的 DirectByteBuffer |

Metaspace OOM:现象是运行一段时间后类加载失败。诱因多是动态类生成——CGLib 每代理一个类就生成一个新 Class,脚本引擎(Groovy)每次编译都产新类,类加载器泄漏(第 2 章的内部类变体:整个 ClassLoader 连同它的类被缓存拖住)。jstat -gcmetacapacity 看趋势,直线上涨即立项。
unable to create new native thread:不是堆的问题。线程栈总占用(线程数 × Xss)加上进程其他内存超了操作系统限额。第 4 章的 BIO 模型、第 5 章的失控 newCachedThreadPool 都以这个报错收场。排查方向在"谁在创建线程",jstack 数一下同名线程组就有答案。
Direct buffer memory:NIO 的堆外缓冲超限。Netty 等框架用池化管理 DirectByteBuffer,泄漏症状是堆很健康、进程 RSS 却一直涨。监控要看进程级内存而非只有堆。
还有一个阴险的变体:Finalizer 队列堵塞。对象的 finalize 方法由单线程的 Finalizer 线程执行,某个 finalize 里做了慢操作(甚至死循环),队列越积越多,对象名义上不可达却回收不掉,表现为"堆里有大量待 finalize 的对象"的伪泄漏。finalize 已在 JDK 18 被标记废弃,新代码用 Cleaner 或显式 close,这是最好的墓志铭。
以堆 OOM 为例的标准四步:
1. 确认类型与时机 GC 日志 -Xlog:gc* 看是缓慢上涨 泄漏 还是瞬时打爆 大查询 2. 拿 dump -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath 自动留现场 或存活时 jmap -dump:live,format=b,file=heap.hprof pid 3. MAT 分析 Dominator Tree 找支配大对象 Leak Suspects 看报告 沿引用链向上找 GC Root 对照代码定位持有者 4. 分类处置 容量型 改分页流式 参数只是缓冲 结构性改掉整载 泄漏型 修引用 缓存加上限与过期 内部类静态化 第2章
导出事故的最终修复正是"结构型":查询改游标分页,Excel 用流式写(每写满一批刷盘释放),单次导出内存占用从 GB 级降到 MB 级——参数永远救不了结构。
⚠️ 常见坑:OOM 后立即重启毁灭现场。没有 dump 的 OOM 复盘全靠猜——生产一律预配
HeapDumpOnOutOfMemoryError,dump 文件大,注意磁盘容量与清理策略。
💡 关键直觉:OOM 报错的后半句就是内存地图上的地名。先读地名再动手,比直接调参少走一半弯路。
下一节看内存的另一半故事:GC 停顿。