首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【JVM】堆 OOM 与 OOMKilled 的分岔口:从堆上限到容器限额

【JVM】堆 OOM 与 OOMKilled 的分岔口:从堆上限到容器限额

原创
作者头像
爪哇碎碎念
修改于 2026-11-08 14:51:04
修改于 2026-11-08 14:51:04
250
举报

两个都叫 OOM,处理方式却几乎是相反的:一个是 JVM 自己抛异常,你还有 GC 日志和堆转储可查;另一个是内核直接把进程杀掉,连日志都来不及刷。走错方向,调参不仅没用,还会加速故障。

先把三条结论拿走

一、分岔口只看报错来自哪一层。 出现 OutOfMemoryError、GC 日志有 Full GC、能拿到堆转储,是 Java 堆那条路;出现退出码 137、Reason: OOMKilled、Last State: Terminated,是内核那条路。

二、GC 日志正常,不能给容器侧免责。 堆 OOM 查「谁在持有对象」;容器 OOMKilled 查「整个进程一共向系统要了多少内存」。后者记的是整个 cgroup 的账,堆只是其中一行。

三、把 MaxRAMPercentage 从 25% 调到 75% 会更容易被杀,不是更安全。 它抬高的是堆上限,挤掉的是堆外余量;而且只要显式写了 -Xmx,百分比立刻失效。

一号路径:Java 堆 OOM

本机实测(JDK 17.0.20.1 / 16 核):-Xmx64m,让唯一的 worker 休眠占住位置,无界队列只进不出,然后不断提交什么都不做的空任务。提交到 2733300 个时抛 OOM,此刻队列长度同样是 2733300。

任务体是空的,内存全花在队列节点和任务对象本身上。每个排队任务都被队列强引用着,GC 没有任何理由回收它们——所以队列长度曲线和堆占用曲线几乎是同一条。

这条路的证据都在 JVM 里:堆转储、GC 日志、集合或队列的长度。动作也落在 JVM 里:给队列设上界、把拒绝策略显式写出来、看住 getLargestPoolSize() 这类计数器。

二号路径:容器 OOMKilled

同一台机器的另一组实测,先看堆上限由谁决定(15.36 GiB 内存):

参数组合

MaxHeapSize

折合

默认 / 25%

4,123,000,832 B

3.84 GiB

75%

12,373,196,800 B

11.52 GiB

-Xmx256m + 75%

268,435,456 B

256 MiB

最后一行最关键:一旦显式给了 -Xmx,flag 来源就从 ergonomic 变成 command line,百分比参数不再参与计算。很多「我明明配了百分比却没生效」的工单,根因就在这一行。

再看进程实际要了多少内存。-Xmx256m 加 192 MiB 直接内存,用 NMT 摊开分项账本:Total committed 约 542 MiB,其中 Java Heap 约 246 MiB、Other(主要含直接内存)约 192 MiB、GC 约 68 MiB、其余约 36 MiB。

一句话:堆没满,账已经超了。 容器限额管的是堆 + 元空间 + 线程栈 + 直接内存 + GC 自身开销 + 页缓存,不是堆。

按 HotSpot 规则推算(推算,非容器实测):512 MiB 限额下,默认 25% 约 128 MiB 堆 + 约 296 MiB 堆外 ≈ 424 MiB,尚有余量;改成 75% 约 384 MiB 堆 + 同样 296 MiB ≈ 680 MiB,直接超限额。

一张表分清两条路

看什么

堆 OOM

容器 OOMKilled

报错形态

OutOfMemoryError、GC 日志

退出码 137、Reason: OOMKilled

记账范围

Java 堆

整个 cgroup 进程

第一眼证据

堆转储、队列长度

NMT Total、memory.current、内核事件

调大堆上限

有时能缓解

常加速被杀

三条能直接跑的诊断命令

代码语言:bash
复制
# 看堆上限到底由谁决定(对比加与不加 -Xmx)
java -XX:+PrintFlagsFinal -version | grep MaxHeapSize
java -Xmx256m -XX:MaxRAMPercentage=75.0 -XX:+PrintFlagsFinal -version | grep MaxHeapSize

# 把进程的分项账本摊开(需启动时加 -XX:NativeMemoryTracking=summary)
jcmd <pid> VM.native_memory summary

# 容器侧确认死因
kubectl describe pod <pod> | grep -A3 'Last State'

第一条的两行输出对比,就是「百分比失效」最省事的复现;完整可运行的队列 OOM 探针代码见 11-07 那篇长文,本文不重复贴。

三个最容易踩的坑

  1. 把百分比调大当成「给更多内存」。容器侧的真实效果是把堆外余量挤掉,把故障从可捕获、可留堆转储的 OutOfMemoryError,换成内核层直接 SIGKILL 的 OOMKilled。
  2. -Xmx 与百分比同时写,以为两者会叠加。实测显示 flag 来源变成 command line,百分比不参与。
  3. 把「堆还有一半空」当免责证据。堆外那 192 MiB 直接内存,堆 GC 日志里完全看不到,照样能把总账推过限额。

边界与版本前提

全部实测在 JDK 17.0.20.1 / 15.36 GiB 本机(Windows) 完成,度量口径是 PrintFlagsFinal 与 NMT;512 MiB 容器限额那组是规则推算,本文未做容器实测。NMT 的 committed 是「已向 OS 承诺的虚拟内存」,与内核记账的 memory.current 不是同一个量,不能划等号。结论只在标注的版本与运行形态下成立,发现不一致欢迎纠错。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 先把三条结论拿走
  • 一号路径:Java 堆 OOM
  • 二号路径:容器 OOMKilled
  • 一张表分清两条路
  • 三条能直接跑的诊断命令
  • 三个最容易踩的坑
  • 边界与版本前提
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档