两个都叫 OOM,处理方式却几乎是相反的:一个是 JVM 自己抛异常,你还有 GC 日志和堆转储可查;另一个是内核直接把进程杀掉,连日志都来不及刷。走错方向,调参不仅没用,还会加速故障。
一、分岔口只看报错来自哪一层。 出现 OutOfMemoryError、GC 日志有 Full GC、能拿到堆转储,是 Java 堆那条路;出现退出码 137、Reason: OOMKilled、Last State: Terminated,是内核那条路。
二、GC 日志正常,不能给容器侧免责。 堆 OOM 查「谁在持有对象」;容器 OOMKilled 查「整个进程一共向系统要了多少内存」。后者记的是整个 cgroup 的账,堆只是其中一行。
三、把 MaxRAMPercentage 从 25% 调到 75% 会更容易被杀,不是更安全。 它抬高的是堆上限,挤掉的是堆外余量;而且只要显式写了 -Xmx,百分比立刻失效。
本机实测(JDK 17.0.20.1 / 16 核):-Xmx64m,让唯一的 worker 休眠占住位置,无界队列只进不出,然后不断提交什么都不做的空任务。提交到 2733300 个时抛 OOM,此刻队列长度同样是 2733300。
任务体是空的,内存全花在队列节点和任务对象本身上。每个排队任务都被队列强引用着,GC 没有任何理由回收它们——所以队列长度曲线和堆占用曲线几乎是同一条。
这条路的证据都在 JVM 里:堆转储、GC 日志、集合或队列的长度。动作也落在 JVM 里:给队列设上界、把拒绝策略显式写出来、看住 getLargestPoolSize() 这类计数器。
同一台机器的另一组实测,先看堆上限由谁决定(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、内核事件 |
调大堆上限 | 有时能缓解 | 常加速被杀 |
# 看堆上限到底由谁决定(对比加与不加 -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 那篇长文,本文不重复贴。
全部实测在 JDK 17.0.20.1 / 15.36 GiB 本机(Windows) 完成,度量口径是 PrintFlagsFinal 与 NMT;512 MiB 容器限额那组是规则推算,本文未做容器实测。NMT 的 committed 是「已向 OS 承诺的虚拟内存」,与内核记账的 memory.current 不是同一个量,不能划等号。结论只在标注的版本与运行形态下成立,发现不一致欢迎纠错。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。