6.1 一次OOM的内存布局透视


文档摘要

6.1 一次 OOM 的内存布局透视 本节摘要:OOM 不是一种故障,是一族。本节从一次订单导出的堆 OOM 讲起,画出运行时数据区全景,逐个对应 OOM 报错与故障区域(堆、元空间、栈、直接内存、Finalizer),讲清堆的分代结构与大对象规则,最后走一遍从 GC 日志到 MAT 的完整排查动作。 事故现场:一次订单导出拖垮整个服务 运营在管理后台点了"导出全年订单",十几分钟后服务雪崩。日志里: 恢复后复盘代码:导出接口把 300 万行订单一次性查进内存拼 Excel,一行按 2KB 算就是 6GB,4GB 的堆毫无机会。更糟的是这发生在与在线交易共享的实例上——一次后台操作放倒了整个交易链路。

6.1 一次 OOM 的内存布局透视

本节摘要: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

JVM 运行时内存布局透视

JVM 运行时内存布局透视

三个非典型 OOM:更容易误诊的三兄弟

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 报错的后半句就是内存地图上的地名。先读地名再动手,比直接调参少走一半弯路。

防坑清单

  • 生产预配 OOM 自动 dump + GC 日志,这是复盘的案发现场照片
  • 大结果集一律分页或流式,"查出全部再处理"是容量事故第一名
  • 缓存必须有上限(LRU/TTL)与大小监控,无界缓存是慢性 OOM
  • Metaspace 看动态类生成趋势,Direct 看进程 RSS,别只盯堆
  • finalize 不写新代码,资源用 try-with-resources

本节要点回顾

  • OOM 分型:堆/元空间/线程/直接内存各有诱因与工具
  • 分代结构:Eden-Survivor-老年代,复制算法清新生代,大对象直入老年代
  • 排查流:GC 日志定时机,dump 配 MAT 支配树找元凶,引用链回溯持有者
  • 处置分类:容量型改结构,泄漏型修引用
  • Finalizer:单线程队列堵塞的伪泄漏,已废弃别再用

下一节看内存的另一半故事:GC 停顿。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U