Java面试要点:内存溢出、堆Dump、GC日志与对象泄漏分析
前言
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 | -XX:+HeapDumpOnOutOfMemoryError |
JDK 8 常见 GC 日志参数:
1 | -XX:+PrintGCDetails |
出现 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 分析、引用链、业务触发点和修复方案。


