📌 PDF :大白话说Java面试题 --- 02-JVM篇
第25题:谈谈对 OOM 的认识
📚 回答:
- 核心考点 :
OOM(OutOfMemoryError)是JVM内存耗尽的终极表现。大厂面试要求能区分不同内存区域的OOM 、常见原因 、排查手段 ,以及为什么OOM后程序不一定立即退出。
1. OOM 的完整分类(按区域)
| OOM类型 | 错误信息 | 对应内存区域 | 典型场景 |
|---|---|---|---|
| 堆内存溢出 | Java heap space |
堆(Heap) | 内存泄漏、对象峰值过大 |
| 元空间溢出 | Metaspace |
元空间(Metaspace) | 动态类加载(热部署、CGlib代理) |
| GC超限 | GC overhead limit exceeded |
堆 | 98%时间GC但回收不到2%内存 |
| 直接内存溢出 | Direct buffer memory |
直接内存(Direct Memory) | NIO/BIO中ByteBuffer.allocateDirect泄漏 |
| 无法创建本地线程 | unable to create new native thread |
栈(Native Thread) | 线程数超系统上限 |
| 栈溢出(不同概念) | StackOverflowError |
虚拟机栈 | 递归过深 |
注意 :
StackOverflowError虽然是内存耗尽,但不属于OOM,而是单独的错误。但面试常混问。
2. 堆内存溢出(最常见)
触发条件 :
新对象分配时,堆内存已满 + GC回收后仍不足。
代码示例(生产常见):
java
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // 1MB
}
根因分类:
- 内存泄漏:对象意外被强引用无法回收(如HashMap未remove、静态集合缓存无清理)
- 内存峰值:请求处理时创建大对象(如批量导入Excel)
排查命令:
bash
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump.hprof
3. 元空间溢出
触发条件 :
加载的新类超过 -XX:MaxMetaspaceSize 限制。
高危场景:
- 热部署(Spring DevTools、Tomcat reload):每次部署加载新ClassLoader,旧类未卸载
- 动态代理 :CGLIB或Javassist生成大量代理类(如Spring中
@Configuration类) - JSP(旧项目):每个JSP编译成单独的类
验证方式:
bash
jstat -gc <PID> | grep -E 'M|Metaspace'
# M (Metaspace) 使用率持续增长直到触发FGC
4. GC overhead limit exceeded
错误信息 :
java.lang.OutOfMemoryError: GC overhead limit exceeded
触发条件 :
JVM检测到 GC时间 > 98% 且 回收内存 < 2%,持续多次后抛出。防止系统因无效GC彻底卡死。
典型场景:
- 堆内存接近满,但每次GC只能回收极少量对象
- 通常是堆太小或存在大量临时对象
解决:
- 不推荐关闭此检测(
-XX:-UseGCOverheadLimit) - 应增大堆或排查泄漏
5. 直接内存溢出
错误信息 :
java.lang.OutOfMemoryError: Direct buffer memory
触发条件 :
ByteBuffer.allocateDirect() 分配的堆外内存超过 -XX:MaxDirectMemorySize(默认等于-Xmx)。
常见原因:
- NIO框架(Netty、Jetty)未及时释放DirectByteBuffer
- 使用
ThreadLocal缓存DirectBuffer导致泄漏
排查:
bash
# 查看直接内存使用量(需jdk8u40+)
jdk/bin/jcmd <PID> VM.native_memory summary
6. 无法创建本地线程
错误信息 :
java.lang.OutOfMemoryError: unable to create new native thread
触发条件 :
系统总线程数超上限 (Linux通常受/proc/sys/kernel/threads-max或内存限制)。
根因:
- 显式创建海量线程(如线程池无限增长)
- 每个线程栈(
-Xss)占用内存过大,导致即使线程数不多也会触发
公式 :
最大线程数 ≈ (进程最大内存 - 堆 - 元空间) / (-Xss)
7. OOM排查标准流程(大厂必问)
| 步骤 | 操作 | 工具/参数 |
|---|---|---|
| 1. 保留现场 | 启动时加 -XX:+HeapDumpOnOutOfMemoryError |
自动dump |
| 2. 获取dump文件 | 从 -XX:HeapDumpPath 路径找到.hprof |
MAT / VisualVM / JProfiler |
| 3. 分析泄漏对象 | 查看Dominator Tree 、Leak Suspects | MAT自动报告 |
| 4. 定位代码行 | 分析GC Roots到泄漏对象的最短路径 | OQL查询 |
| 5. 复现与修复 | 压测验证 | JMeter等 |
无dump时的现场信息:
bash
jstat -gcutil <PID> 1000 # 看各区域使用率
jmap -histo:live <PID> # 存活对象统计(会STW,慎用)
8. OOM后程序一定会退出吗?
答:不一定。
-
如果OOM发生在非主线程 (如线程池任务内),该线程抛出
OutOfMemoryError后终止 ,但其他线程(包括主线程)可能继续运行。 -
危险:OOM后系统状态不一致,继续处理请求可能导致数据损坏。
-
生产最佳实践 :
bash-XX:OnOutOfMemoryError="kill -9 %p" # OOM时直接杀进程或通过健康检查(如k8s liveness probe)自动重启。
9. 面试官追问示例与回答
Q1:Java heap space 和 GC overhead limit exceeded 什么区别?
A:前者是堆满且GC后仍无空间 分配新对象;后者是GC一直在工作但无效(98%时间GC回收<2%内存),属于JVM的保护机制。
Q2:元空间溢出只加 MaxMetaspaceSize 就能解决吗?
A:治标不治本。应排查是否存在类加载器泄漏(如Tomcat热部署后原ClassLoader未卸载),结合 jmap -clstats 查看类加载器数量。
Q3:直接内存溢出如何定位?
A:用 jcmd <PID> VM.native_memory summary 看 Internal 区域,或使用 -XX:NativeMemoryTracking=detail 跟踪。Netty可开启 leakDetectionLevel。
Q4:OOM后还能执行 finally 块吗?
A:OOM是 Error,finally 会执行 (如果OOM发生在 try 内部)。但若OOM发生在 finally 内部则不会执行完。
💡 面试官想要的满分总结:
"OOM按区域分为堆、元空间、直接内存、本地线程等类型。
必须开启-XX:+HeapDumpOnOutOfMemoryError保留现场。排查路径:dump → MAT分析Leak Suspects → 定位GC Roots → 修复引用链。
注意:非主线程的OOM不会导致进程退出,需通过
-XX:OnOutOfMemoryError=kill -9 %p确保快速自愈。常见误区:StackOverflowError不是OOM;GC overhead limit exceeded也是OOM的一种;元空间溢出常与类加载器泄漏有关,而非单纯空间太小。"
觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯