前言

Java 内存溢出不是只有 java.lang.OutOfMemoryError: Java heap space。生产环境还可能出现元空间溢出、直接内存溢出、线程创建失败、GC overhead limit 等问题。面试时要能区分不同 OOM 类型,并讲清楚怎么定位。

常见 OOM 类型

类型 常见报错 典型原因
堆内存溢出 Java heap space 对象泄漏、大集合、缓存无限增长
GC 开销过高 GC overhead limit exceeded GC 回收效果很差
元空间溢出 Metaspace 动态类太多、类加载器泄漏
直接内存溢出 Direct buffer memory NIO、Netty、ByteBuffer 未释放
线程创建失败 unable to create new native thread 线程过多、线程池失控

排查流程

flowchart TD
    A[出现OOM] --> B[确认OOM类型]
    B --> C[保留日志和Dump]
    C --> D[分析GC日志]
    C --> E[分析堆Dump]
    E --> F[查看大对象]
    E --> G[查看引用链]
    E --> H[查看类加载器]
    D --> I[判断是否频繁GC]
    F --> J[定位代码和业务场景]
    G --> J
    H --> J

现场保留

建议生产 JVM 增加参数:

1
2
3
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dump
-Xlog:gc*:file=/data/logs/gc.log:time,level,tags

JDK 8 常见 GC 日志参数:

1
2
3
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/data/logs/gc.log

出现 OOM 后,不要只重启。至少保留:

  • 应用日志。
  • GC 日志。
  • heap dump。
  • 容器内存限制。
  • JVM 启动参数。
  • 最近发布和配置变更记录。

堆 Dump 分析重点

常用工具是 MAT、JProfiler、VisualVM、Arthas heapdump。

重点看:

  • Dominator Tree。
  • Retained Heap 最大的对象。
  • Histogram 中数量异常的类。
  • GC Roots 引用链。
  • 是否存在自定义缓存、Map、List、队列无限增长。
  • 是否存在 ThreadLocal 未清理。

如果一个 ConcurrentHashMap 占用了大量内存,要继续看它被谁持有、key 是什么、value 是什么、是否有过期策略。

常见泄漏场景

本地缓存无上限

业务为了减少 DB 查询,在内存里放了 Map,但没有最大容量和过期策略。流量增长后,Map 持续膨胀。

处理方式:

  • 使用 Caffeine 等带淘汰策略的缓存。
  • 设置最大容量。
  • 设置过期时间。
  • 监控命中率和缓存大小。

ThreadLocal 未清理

线程池中的线程会复用,如果 ThreadLocal 存了大对象且没有 remove,就可能导致对象长期无法释放。

处理方式:

  • 在 finally 中 remove。
  • 不在 ThreadLocal 存大对象。
  • 对用户上下文、租户上下文做统一拦截器清理。

MQ 消费堆积到内存

一次拉取大量消息放入内存,消费速度又跟不上,容易造成堆膨胀。

处理方式:

  • 控制批量拉取大小。
  • 分批处理。
  • 失败消息落库或死信。
  • 监控队列长度和内存占用。

大文件一次性加载

Excel 导入、文件下载、报表导出如果一次性读入内存,数据量变大后容易 OOM。

处理方式:

  • 流式读取。
  • 分页处理。
  • 异步任务。
  • 临时文件中转。

直接内存排查

直接内存常见于 Netty、NIO、文件传输和堆外缓存。

排查方向:

  • 是否配置 -XX:MaxDirectMemorySize
  • 是否使用 Netty ByteBuf 后未释放。
  • 是否有大量 DirectByteBuffer。
  • 容器内存是否小于 JVM 总内存预估。

堆看起来不高但容器 OOMKilled,要重点怀疑堆外内存、线程栈、Metaspace 和 native 内存。

元空间排查

元空间溢出常见原因:

  • 动态代理类不断生成。
  • Groovy、CGLIB、Javassist 使用不当。
  • 热部署导致类加载器泄漏。
  • 脚本引擎动态加载类没有释放。

排查时看类数量、类加载器数量、Metaspace 增长趋势。

止血方案

  • 临时扩容内存,但这只能缓解,不能替代定位。
  • 关闭异常任务或大批量导出。
  • 清理异常缓存。
  • 降低消费批量和并发。
  • 回滚最近发布。
  • 对高风险接口临时限流。

高频面试题

OOM 怎么排查?

先确认 OOM 类型,保留 GC 日志和堆 Dump。堆 OOM 用 MAT 看大对象、对象数量和 GC Roots 引用链;直接内存和线程 OOM 要看堆外、线程数、JVM 参数和容器限制。

堆内存使用高一定是内存泄漏吗?

不一定。可能是正常高峰流量、批处理大对象、缓存预热或对象分配速率高。是否泄漏要看内存是否持续增长、Full GC 后是否明显回落。

ThreadLocal 为什么会泄漏?

线程池线程长期存活,ThreadLocal 中的 value 如果没有 remove,会跟随线程长期存在,导致请求结束后对象仍然不能释放。

总结

内存问题排查要区分堆、堆外、元空间和线程。面试回答要体现证据链:报错类型、GC 日志、Dump 分析、引用链、业务触发点和修复方案。