跳到主要内容

常见 OOM 与栈溢出场景

OutOfMemoryError 只说明某次资源申请无法满足,未必是 Java 堆泄漏。第一步应保留完整错误消息,确认失败的是堆、类元数据、直接内存、native thread 还是其他资源,再选择证据。

1. Java heap space

List<byte[]> retained = new ArrayList<>();
while (true) {
retained.add(new byte[1024 * 1024]);
}

常见原因:

  • 无界缓存、队列或批次持续持有对象。
  • 单次请求尝试建立过大的数组或对象图。
  • 工作集本身超过堆容量。
  • 分配速率过高,收集器来不及提供空间。

OOM 不自动证明泄漏。若堆中对象都是业务必须的 live set,问题可能是容量或模型;若稳定负载下 Full GC 后 live set 仍持续增长,才更像意外持有。

建议在受控环境验证 -XX:+HeapDumpOnOutOfMemoryError 和转储路径。堆转储可能很大并造成额外停顿,生产环境要预留磁盘与采集策略。

2. GC overhead limit exceeded

HotSpot 在绝大多数时间用于 GC、每次只回收很少空间时,可以抛出这个错误。它描述的是进展不足,不是一个独立内存区域。

需要同时看:

  • Full/old collection 后的 live set。
  • 分配和晋升速率。
  • GC CPU 占比与应用吞吐。
  • 堆是否过小,或是否有大规模对象滞留。

关闭这个保护只会让进程更久地停留在低进展状态,通常不是修复。

3. MetaspaceCompressed class space

常见来源:

  • 动态生成大量类。
  • 热部署反复创建类加载器,旧加载器仍被线程或静态注册持有。
  • MaxMetaspaceSize 或压缩类空间上限与实际类规模不匹配。
jcmd <pid> VM.classloader_stats
jcmd <pid> VM.metaspace

仅增大 Metaspace 会掩盖类加载器泄漏。要观察加载器数量、每个加载器定义的类以及能否卸载。

4. Direct buffer memory 与 native allocation failure

直接缓冲区 OOM 常来自无界池、过大缓冲区或释放滞后。native heap 申请失败还可能显示 Out of swap spaceNative method 等细节,甚至由 native 代码直接崩溃并生成 hs_err_pid 日志。

这时堆转储可能很小。使用 NMT、直接缓冲区池指标、进程内存映射和 native profiler,不能按 Java 堆泄漏路径排查。

5. unable to create native thread

创建平台线程需要本地线程结构和栈。失败可能因为:

  • 应用无限创建线程或线程泄漏。
  • 线程栈配置过大,进程地址空间或物理内存不足。
  • 操作系统用户/进程线程数限制。
  • 容器 PID 或内存限制。
jcmd <pid> Thread.print
ps -eLf | grep <pid> | wc -l

修复应先控制并发和线程生命周期。盲目减小 -Xss 可能增加递归溢出风险,也不能解决任务没有背压的问题。

6. StackOverflowError

static void recurse() {
recurse();
}

线程请求的栈深度超过实现允许范围时抛出 StackOverflowError。常见原因是无终止递归、异常处理再次递归、对象 toString/equals 的循环调用,或正常递归在过小栈上运行。

先看重复栈帧定位调用环。如果算法深度确实随输入增长,优先改为显式栈或迭代;增大 -Xss 只适合最大深度可控且容量预算允许的场景。

7. 一套按错误类型选择证据的流程

现象首要证据
Java heap spaceGC 时间线、class histogram、heap dump
Metaspace类加载器统计、类加载/卸载趋势、NMT
Direct buffer memoryBufferPool 指标、NMT、native 内存映射
unable to create native thread线程转储、OS/PID 限制、每线程栈预算
StackOverflowError异常栈中的重复调用环
进程被 OOM Kill容器/内核事件、RSS 与各内存类别时间线

所有场景都先保存首次错误、启动参数、容器限制和同一时间窗口的指标。故障后重复触发 Full GC 或连续导出堆,可能让现场进一步恶化。

8. 常见问题

8.1 内存泄漏和内存溢出有什么区别

泄漏是程序意外保留已经无用的对象或资源;溢出是一次申请无法满足。泄漏可以最终导致溢出,但合理的工作集超过容量也会溢出,没有泄漏。

8.2 OOM 后进程一定退出吗

不一定。OutOfMemoryErrorError,可能只终止触发它的线程,也可能被捕获;但此时进程资源紧张,错误处理也可能分配失败。关键服务通常应通过外部监督安全重启,并在退出前尽可能保留低风险证据。

8.3 System.gc() 能解决 OOM 吗

它只是向 JVM 建议执行 GC,可能被忽略,也不能回收仍然可达的对象。频繁调用还可能带来昂贵停顿,不是容量或泄漏修复手段。

9. 面试题

9.1 StackOverflowError 和不同 OOM 分别怎样产生

出现公司:阿里巴巴、腾讯

考察重点

  • 栈深度、堆、类元数据、直接内存和线程资源。
  • 完整 detail message 的诊断价值。
  • OOM 不等于内存泄漏。

相关内容:第 1 节“Java heap space”至第 6 节“StackOverflowError”。

参考回答

递归或调用链超过线程栈可承受深度时通常是 StackOverflowError。OOM 要看详细消息:堆无法分配对象是 Java heap space,类元数据到达限制是 Metaspace,直接缓冲区受限是 Direct buffer memory,平台线程创建失败可能是 unable to create native thread

这些错误选择的证据不同。堆看 histogram 与 dump,类元数据看加载器,直接和 native 内存看 NMT 与系统工具,线程失败看线程数量、栈预算和 OS 限制。

9.2 线上发生 OOM 应该怎样定位

出现公司:阿里巴巴、高德

考察重点

  • 先分类再采证。
  • 泄漏、容量不足和分配过快的区分。
  • 采集动作本身的停顿与磁盘风险。

相关内容:第 7 节“一套按错误类型选择证据的流程”。

参考回答

先保留完整错误、启动参数和容器事件,根据 detail message 判断资源类别。堆 OOM 关联 GC 后 live set、分配率、histogram 和 heap dump;Metaspace 检查加载器及类卸载;direct/native 检查 NMT、BufferPool 与系统映射;线程 OOM 检查线程转储和 OS 限制。

最后区分意外持有、容量确实不足和短时分配峰值。生产采集要预估停顿与磁盘,避免在已失稳进程上连续导出大堆。