快速修复
# 1. 先开堆 dump(下次 OOM 自动落盘),比瞎猜快
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/app.hprof -jar app.jar
# 2. 看 GC 日志确认是哪种耗尽
java -Xlog:gc -jar app.jar # 或 -verbose:gc
# 3. 常见立竿见影的改法(按签名对号入座)
# heap space + 大文件下载 -> 改流式输出/外链,不整体读 byte[]
# heap space + 大查询 -> 分页 + 缓存
# 线程耗尽 -> 有界线程池;logback discardingThreshold 调回默认(别设 0)
# 随机位置 OOM -> 检查 -Xmn,别等于 -Xmx
你会遇到什么
Java 服务突然挂掉/502/接口 500,日志里是各种 OOM。不同签名指向完全不同的根因,先对号入座再动手:
| 报错签名 | 典型根因 | 排查方向 |
|---|---|---|
OutOfMemoryError: Java heap space | 堆被大对象/大集合填满:一次读 200MB 文件进 byte[]、无分页查 10w+ 行、ZIP 炸弹内存解压、-Xmn 与 -Xmx 同值失衡 | 堆 dump + MAT,看谁占内存;检查大文件/大查询/解压 |
OutOfMemoryError: Metaspace | 启动时一次性加载全量 XSD/类(JAXB newSchema 全量初始化),类元数据塞满 Metaspace | 检查启动加载的 schema/class 数量;扩容或分块加载 |
StackOverflowError | 递归无出口:信任第三方参数(传了 -1)导致递归不终止 | 参数校验 + 递归改循环 |
OutOfMemoryError: unable to create new native thread | 线程数超系统上限:无界线程池 + 日志阻塞(磁盘满 + logback AsyncAppender 队列满)→ 线程堆积 | jstack 看线程数/栈;磁盘、日志配置、线程池上限 |
OutOfMemoryError: GC Overhead limit exceeded | GC 空转零回收:ThreadLocal 未清理等内存泄漏,Full GC 也回收不了 | MAT 看 Incoming references(如 ThreadLocalMap) |
| 不同位置随机 OOM | JVM 参数失衡:-Xmn(年轻代)设成与 -Xmx 一样大 | 检查启动脚本 -Xmn/-Xmx 比例 |
本文路径:快速修复(30 秒一段命令)→ 一步步排查(可跳读)→ 原理与变体(症状/原因/证据)。赶时间直接跳到「快速修复」。
为什么会这样
JVM 内存分堆/元空间/线程栈/本地线程四块,各自独立耗尽:
| 区域 | 耗尽机制 |
|---|---|
| 堆(Java heap) | 对象持续累积:大文件整体读入、无分页大查询、ZIP 炸弹(极小压缩包解压出 1000 倍以上数据) |
| Metaspace | 类元数据:一次性 newSchema 全量 XSD → 大量类加载 |
| 线程栈 | 递归过深:StackOverflowError |
| 本地线程 | 系统线程数上限(ulimit -u / cat /proc/sys/kernel/threads-max):线程池无界 + 业务线程被日志写阻塞(磁盘满 + AsyncAppender 队列满且 discardingThreshold=0)→ 只进不出 |
| GC 空转 | 泄漏对象无法回收:Full GC 回收 <2% 却占用 >98% 时间 → GC Overhead limit |
JVM 的「内存」不是一块,是四个独立耗尽通道,每个的 OOM 文案和修法都不同:堆(Java heap)由 GC 管理,对象持续累积才耗尽;Metaspace 存类元数据,动态生成类(如全量 XSD→类)才耗尽;线程栈是每线程固定大小(默认 1MB 级),递归过深耗尽(StackOverflowError);
本地线程受操作系统线程数上限约束(ulimit -u / threads-max),线程池无界 + 线程只进不出(被日志写阻塞)就耗尽。
GC 空转(回收 <2% 却占 >98% 时间)是「堆有大量不可达但未回收对象」的指纹。
确定性推论:OOM 文案里的区域名(Java heap space / Metaspace / unable to create native thread / GC overhead)直接决定排查方向——堆 OOM 查对象累积与泄漏,Metaspace OOM 查类加载数量,native thread OOM 查线程数与阻塞,不要统一「加 -Xmx」;判断路径:先读异常类名再动手。
一步步来
1. 记录签名(先判断是哪一类) — 必做
Java heap space / Metaspace / StackOverflowError / unable to create new native thread / GC Overhead limit exceeded / Direct buffer memory——签名不同,武器不同。
2. 复现与取证 — 必做
- 开启
-XX:+HeapDumpOnOutOfMemoryError,OOM 时拿到 .hprof; jmap -dump:format=b,file=app.hprof <pid>手动抓(服务还活着时);- MAT/Eclipse MAT 打开 dump,看 Dominator Tree 大对象 + Incoming references(案例八:ThreadLocal 泄漏 = Incoming references 指向 ThreadLocalMap);
jstack <pid>看线程栈(native thread 耗尽时看线程总数)。
3. 按根因修 — 必做
| 根因 | 修法 |
|---|---|
大文件整体读入内存(readFileContent 全量 byte[]) | 流式读写(FileInputStream→OutputStream 缓冲循环)或文件挂 FTP/对象存储外链,不经过应用进程 |
| 无分页大查询(10w+ 行反复查) | 限制行数 + 查询缓存 + 缩短轮询周期 |
| ZIP 炸弹(压缩包极小、解压巨大) | 禁嵌套压缩包、校验文件大小与路径、解压到磁盘临时目录并限制总量 |
| Metaspace(全量 XSD 一次性加载) | 临时扩容 -XX:MaxMetaspaceSize;根治:服务拆分/分块按需加载 schema |
| 递归无出口(第三方传 -1) | 严格参数校验 + 递归改循环 |
| 无界线程池 + 日志阻塞 | 有界线程池;AsyncAppender discardingThreshold 调回合理值(0 = 绝不丢日志,也绝不阻塞保护,磁盘满时线程被拖死) |
-Xmn = -Xmx | 恢复合理比例(年轻代约为堆的 1/3~1/2,按业务调) |
| ThreadLocal 泄漏 | 用后 remove(finally 里),避免线程池复用导致引用滞留 |
4. 验证 — 必做
压测/复现原触发路径,确认无 OOM;GC 日志平稳;jstack 线程数回到正常水位。
怎么确认修好了
同一触发路径下不再 OOM,GC 日志无连续零回收 Full GC,线程数稳定;堆 dump 中不再出现原大对象/泄漏引用。
其它环境的表现
| 环境 | 说明 |
|---|---|
| 现代 JVM(G1/ParallelGC) | 小堆快速分配时 GC Overhead 签名可能直接渲染成 Java heap space(本机实测),别只盯字面签名 |
| 未实测 | Metaspace / native thread / 无界线程池场景本机未复现,机制与修法来自原文案例二/四 |
症状
Java 服务 OOM:Java heap space(大文件/大查询/ZIP 炸弹/-Xmn 失衡)、Metaspace(全量 schema 加载)、StackOverflowError(递归无出口)、unable to create new native thread(线程耗尽)、GC Overhead limit exceeded(GC 空转)。
原因
JVM 内存各区域独立耗尽:堆被大对象/大集合填满;Metaspace 被类元数据塞满;线程栈递归过深;本地线程数超系统上限;泄漏对象导致 GC 空转零回收。
解法
- 先开
-XX:+HeapDumpOnOutOfMemoryError+ GC 日志; - 按报错签名对号入座(见速查表);
- 大文件流式化/外链、大查询分页+缓存、禁 ZIP 炸弹(大小/嵌套/路径校验)、schema 分块加载、参数校验+递归改循环、有界线程池+日志不阻塞、-Xmn 合理比例、ThreadLocal 用后 remove;
- MAT/jstack 取证后修,压测验证。
验证证据
- 机制实测(2026-08-30,WSL Ubuntu,OpenJDK 17.0.18):①
-Xmx64m无限分配 →OutOfMemoryError: Java heap space;② 无限递归 →StackOverflowError(栈深 22691);③ ZIP 炸弹(200MB 重复字符压缩成 204KB,膨胀比 >1000x)内存解压 →Java heap space(对应案例一机制);④-Xmx16m慢速泄漏 → 16s 内 31 次 Full GC、14M→14M 零回收(GC 空转机制实测,字面 GC Overhead 签名未稳定复现)。证据:实测复现日志(2026-08-30)。 - 未实测:Metaspace / native thread / 无界线程池 + logback 阻塞场景(无对应业务环境),机制与修法来自原文案例二/四。
适用工具 (1)
本问题方案已挂到下列工具的方案页——从工具页也能回到这里,双向可达。
jdk · 外部工具来源与证据
素材来源:https://www.cnblogs.com/mylibs/p/19321491/production-accident-vl01
← 编码代理与终端 问题域 | 问题货架 | 遇到其它问题?去 问题空间 提交求助卡