我的 Java 服务一上 K8s 就莫名其妙地被反复重启、退出码永远是 137,我对着 OOMKilled 这个状态和容器内存限制排查了大半天才搞懂的惨痛经历

我的 Java 服务在虚拟机上稳如老狗,一上 K8s 就反复重启,kubectl describe 显示 Reason: OOMKilled、Exit Code: 137。我给容器配了 4G limit、虚拟机上 4G 也够用,怎么会内存不足?深挖才懂:JVM 在容器里"看不见"cgroup 的内存 limit——容器 limit 是 4G 但节点宿主机有 64G,我没设 -Xmx 也没开容器感知,老版本 JDK 就按宿主机 64G 的 1/4=16G 设了堆,JVM 以为有 16G 放心分配,实际一越过 4G 红线就被内核 OOM Killer 直接 SIGKILL(137=128+9)。更隐蔽的是 JVM 总内存=堆+非堆(Metaspace/线程栈/直接内存/native),即使 -Xmx 等于 limit 加上非堆照样超。这篇从容器内存限制与 JVM 内存近视讲起,到 UseContainerSupport+MaxRAMPercentage 或显式 -Xmx 并给非堆留余量的正解、容器 OOM 与堆 OOM 之分、退出码速查与资源配置误区,以及那句最戳心的——容器不是虚拟机而是被 cgroup 限制的进程,迁移时要质疑程序的每一个隐性假设、让它看清真实身处的世界。

我的 Java 服务一上 K8s 就莫名其妙地被反复重启、退出码永远是 137,我对着 OOMKilled 这个状态和容器内存限制排查了大半天才搞懂的惨痛经历

这是一个让我对"容器里的内存限制"彻底改观的故事。我有一个 Java 服务,在物理机/虚拟机上,跑得稳如老狗,从来不出问题。可当我把它打包成容器、部署到 K8s 上之后,怪事就发生了:它会莫名其妙地被反复重启kubectl get pods 一看,RESTARTS 的次数蹭蹭上涨;kubectl describe pod 一查,赫然写着一行:Last State: Terminated, Reason: OOMKilled, Exit Code: 137

我当时就纳闷了:OOMKilled?内存不足被杀?可这服务在虚拟机上用 4G 内存跑得好好的,我给容器也配了 4G 的 limit 啊,怎么会内存不足?我顺着 OOMKilled 和退出码 137 这两个线索深挖,才终于揭开真相,补上了我对"容器化"一个致命的认知盲区:问题的核心,是 JVM(以及很多老程序),在容器里,"看不见"容器的内存限制。我的容器 limit 设的是 4G,可这台 K8s 节点(宿主机)的物理内存,有 64G;而我的 JVM,启动时没有显式指定堆大小(-Xmx),于是它就按照"默认规则",去读取它以为的"可用内存"——而在老版本的 JDK / 没正确配置时,它读到的,不是容器那 4G 的 limit,而是宿主机的 64G!于是,JVM 自作主张地,把堆的上限(-Xmx),设成了宿主机内存的 1/4,也就是 16G!悲剧就此注定:JVM 以为自己有 16G 堆可用,于是放心大胆地分配内存;可它实际所在的容器,只有 4G 的硬限制;当 JVM 真实使用的内存,悄悄越过 4G 这条红线时,容器的 cgroup 内存限制就被触发,Linux 内核的 OOM Killer毫不留情地,把这个超限的进程,直接 SIGKILL 杀死(退出码 137 = 128 + 9,9 就是 SIGKILL)——这,就是 OOMKilled 反复重启的真相!我这才痛彻地明白:容器,不是一个"完整的小机器",它只是宿主机上一个"被 cgroup 限制了资源的进程组";那些诞生于"独占整台机器"时代的老程序(尤其是 JVM),如果不被明确告知"你现在被关在一个有内存上限的笼子里",它们就会按宿主机的庞大资源去自我规划,从而必然撑爆那个小小的容器。容器化,绝不是"把程序塞进容器就完事",更要让程序"感知"到它所处的资源边界

故障现场:JVM 看不到容器 limit,按宿主机内存设堆

我把这个"OOMKilled"的现场,用配置和命令摊开给你看:

# ✗ 灾难: 容器限制 4G, 但 JVM 没被告知, 按宿主机 64G 设堆
# K8s Deployment 片段:
resources:
  limits:
    memory: "4Gi"      # 容器硬限制 4G —— 超了就 OOMKilled
  requests:
    memory: "4Gi"

# 启动命令(✗ 没有指定 -Xmx, 也没开容器感知):
# java -jar app.jar
#   → 老版本 JDK / 未配置时, JVM 读到的"可用内存"是宿主机的 64G!
#   → JVM 默认把 -Xmx 设为 内存的 1/4 = 16G(远超容器 4G limit!)

# 进容器一看就露馅:
# $ java -XX:+PrintFlagsFinal -version | grep MaxHeapSize
#   MaxHeapSize = 17179869184   ← 16G! JVM 以为自己能用 16G 堆

# 实际发生:
#   1. JVM 以为有 16G 堆, 放心大胆分配内存。
#   2. 真实内存用量悄悄越过容器 4G 红线。
#   3. cgroup 内存限制触发 → 内核 OOM Killer 把进程 SIGKILL。
#   4. 退出码 137 = 128 + 9(SIGKILL), 容器重启, 周而复始。

# kubectl describe pod 现场:
#   Last State:  Terminated
#   Reason:      OOMKilled
#   Exit Code:   137

# 根因: JVM 看不到容器 cgroup 内存 limit, 按宿主机内存设堆 → 实际超 limit → 被 OOMKilled。

看着这些配置和那个刺眼的 MaxHeapSize = 16G,我才算彻底想明白了这场反复重启的根源。问题的核心,是 JVM 看不到容器的内存 limit:容器 limit 是 4G,可 JVM 启动时没指定 -Xmx、也没开容器感知,在老版本 JDK 下,它读到的"可用内存"是宿主机的 64G,于是把堆上限默认设成了 64G 的 1/4 = 16G悲剧就此发生:第一,JVM 以为自己有 16G 堆,放心大胆分配;第二,真实内存用量悄悄越过容器 4G 的红线;第三,cgroup 内存限制被触发,内核的 OOM Killer 把进程 SIGKILL;第四,退出码 137 = 128 + 9(9 即 SIGKILL),容器重启、周而复始。进容器跑一句 java -XX:+PrintFlagsFinal -version | grep MaxHeapSize,就当场露馅——那个 16G 的堆上限,远超容器的 4G。kubectl describe pod 里的 Reason: OOMKilled, Exit Code: 137,也与之完全吻合。归根结底:JVM 看不到容器 cgroup 的内存 limit,按宿主机的庞大内存去设堆,导致实际用量必然超过 limit,被内核 OOMKilled。

第一件事:搞懂容器内存限制与 JVM 的"内存近视"

定位到根源,我必须把"容器里的内存"这件事,从根上彻底搞清楚:

容器内存限制: cgroup 划定上限, 超了就被内核 OOM Killer 杀

# 容器的本质:
#   - 容器 NOT 虚拟机, 它只是宿主机上"被 cgroup 限制资源的进程"。
#   - memory limit 由 cgroup 实施: 进程内存用量超过 limit → 触发内核 OOM。
#   - 被 OOM Killer 杀 = SIGKILL → 退出码 137(128+9)。

# JVM 的"内存近视"(关键!):
#   - 老版本 JDK(8u131 前)完全不感知 cgroup → 读到宿主机内存。
#   - 没设 -Xmx 时, JVM 按"它以为的可用内存"的 ~1/4 设默认堆。
#   - 宿主机 64G → 堆默认 16G → 远超容器 4G limit → 必然 OOMKilled。

# 更隐蔽的: JVM 内存 ≠ 只有堆!
#   一个 Java 进程的总内存 = 堆(-Xmx) + 非堆:
#     - Metaspace(类元数据)
#     - 线程栈(每线程约 1M, 线程多很可观)
#     - JIT 编译缓存 / 代码缓存
#     - 直接内存 DirectByteBuffer(NIO/Netty 常用, 默认约等于 -Xmx)
#     - GC 自身开销 / native 库内存
#   → 即使 -Xmx 设成 limit, 加上非堆也会超! 必须给非堆留余量。

# 退出码速记:
#   137 = 128 + 9  → SIGKILL(被 OOM Killer 或 kill -9)
#   143 = 128 + 15 → SIGTERM(正常停止/优雅关闭信号)

# 关键认知: 让程序"感知"它所处的资源边界。
#   容器化不是塞进去就行, 要告诉程序"你被限制在多大的笼子里"。

# 核心: 容器内存超 cgroup limit 就被 OOMKilled(退出码137);
#   JVM 默认看不到 limit 按宿主机设堆, 且总内存=堆+非堆, 都要算进 limit。

原理终于清晰了。容器的本质,不是虚拟机,而是宿主机上"被 cgroup 限制了资源的进程";内存 limitcgroup 实施,进程用量一超,就触发内核 OOM、被 SIGKILL(退出码 137)。而 JVM 有"内存近视":老版本 JDK(8u131 之前)完全不感知 cgroup,读到的是宿主机内存;没设 -Xmx 时,它按"以为的可用内存"的约 1/4 设默认堆,宿主机 64G 就设出 16G 堆,必然撑爆 4G 容器更隐蔽的是:JVM 的总内存,远不止堆!一个 Java 进程的内存 = 堆(-Xmx)+ 非堆,而非堆包括 Metaspace、线程栈(每线程约 1M)、JIT 代码缓存、直接内存(DirectByteBuffer,NIO/Netty 常用,默认约等于 -Xmx)、GC 开销、native 库——即使你把 -Xmx 设成等于 limit,加上这些非堆,照样会超!必须给非堆留出余量我还记下了退出码速记:137 = 128 + 9(SIGKILL,被 OOM Killer 杀);143 = 128 + 15(SIGTERM,正常停止)。由此,我刻下一个关键认知:容器化不是"塞进去就行",要让程序"感知"它所处的资源边界——告诉它"你被关在多大的笼子里"。容器内存超 cgroup limit 就被 OOMKilled;JVM 默认看不到 limit、按宿主机设堆,且总内存 = 堆 + 非堆,都要算进 limit

第二件事:正解——让 JVM 感知容器,并给非堆留余量

搞懂了原理,正解就清晰了:要让 JVM 感知到容器的内存限制,并在设堆时给非堆内存留出足够余量

# ✓ 正解一: 开启容器感知(JDK 10+ 默认开; JDK 8u191+ 需显式开)
# JDK 8u191 ~ 8uXXX:
java -XX:+UseContainerSupport \
     -XX:MaxRAMPercentage=75.0 \   # ✓ 堆最多用容器 limit 的 75%, 留 25% 给非堆
     -jar app.jar
#   UseContainerSupport: 让 JVM 读 cgroup limit(而非宿主机内存)。
#   MaxRAMPercentage: 堆占"容器 limit"的百分比(注意是百分比, 不是小数!)

# ✓ 正解二: 干脆显式写死 -Xmx(最稳妥, 不依赖版本行为)
java -Xmx3g -Xms3g \              # ✓ 容器 4G, 堆给 3G, 留 1G 给非堆+系统
     -XX:MaxMetaspaceSize=256m \  # ✓ 限制 Metaspace, 防它无限涨
     -jar app.jar

# ✓ 验证: 进容器确认 JVM 真的看到了 4G(而不是 64G)
java -XX:+PrintFlagsFinal -version | grep -E "MaxHeapSize|MaxRAMPercentage"
#   MaxHeapSize 应约等于 3G(而不是 16G)→ 说明生效了

# 怎么给非堆留余量(经验法则):
#   容器 limit = -Xmx(堆) + Metaspace + 线程栈*线程数 + 直接内存 + GC + native
#   - 小服务: -Xmx ≈ limit * 0.7~0.75, 留 25~30% 给非堆。
#   - 用 Netty/NIO 大量直接内存的: 还要 -XX:MaxDirectMemorySize 限制, 留更多。

# ⚠ 老版本 JDK(8u131 前)不支持 UseContainerSupport:
#   必须老老实实显式 -Xmx, 或升级 JDK。

# 核心: 用 UseContainerSupport + MaxRAMPercentage 让 JVM 感知容器,
#   或直接显式 -Xmx; 并务必给非堆(Metaspace/线程栈/直接内存)留出余量。

修复的方向,清晰而坚定。正解一:开启容器感知——用 -XX:+UseContainerSupport 让 JVM 去读 cgroup 的 limit(而不是宿主机内存),再用 -XX:MaxRAMPercentage=75.0 让堆最多占容器 limit 的 75%(注意这是百分比,不是小数!这是个经典坑),给非堆留出 25%。(JDK 10+ 默认就开了容器感知,JDK 8u191+ 需要显式开。)正解二(最稳妥、不依赖版本):干脆显式写死 -Xmx——容器 4G,堆就给 -Xmx3g,留 1G 给非堆和系统,再用 -XX:MaxMetaspaceSize 限制 Metaspace 防它无限涨。改完一定要验证:进容器跑 java -XX:+PrintFlagsFinal -version | grep MaxHeapSize,确认它真的约等于 3G(而不是 16G),才算生效。怎么给非堆留余量?记住:容器 limit = 堆 + Metaspace + 线程栈×线程数 + 直接内存 + GC + native;小服务,-Xmxlimit 的 70~75%;大量用 Netty/NIO 直接内存的,还要用 -XX:MaxDirectMemorySize 限制、留更多余量。归根结底:UseContainerSupport + MaxRAMPercentage 让 JVM 感知容器,或直接显式 -Xmx;并务必给非堆留出余量。

第三件事:OOMKilled 的排查路径——分清谁 OOM 了

这次踩坑也让我意识到,"OOM"其实有好几种,排查时第一步就是分清是哪一种,否则方向全错:

OOM 排查: 先分清是"容器 OOMKilled" 还是 "JVM 堆 OOM"

# 两种 OOM, 完全不同!
# A. 容器 OOMKilled(本文): 进程总内存超 cgroup limit, 被内核杀。
#    - 现象: 退出码 137, kubectl describe 显示 Reason: OOMKilled。
#    - 没有 Java 异常栈(进程被直接 SIGKILL, 来不及打日志)。
#    - 根因: -Xmx 太大 / 非堆没留余量 / limit 太小 / 内存泄漏。
# B. JVM 堆 OOM: 堆内对象超过 -Xmx, JVM 自己抛异常。
#    - 现象: 日志里有 java.lang.OutOfMemoryError: Java heap space。
#    - 进程可能还活着(看 OOM 行为配置), 有完整异常栈。
#    - 根因: 堆设太小 / 内存泄漏(对象一直被引用不释放)。

# 排查步骤:
# 1. kubectl describe pod  → 看 Last State / Reason / Exit Code
#    137 + OOMKilled → 走 A 路线(容器层面超限)
# 2. 看应用日志有没有 OutOfMemoryError → 有 → 走 B 路线(堆内超限)
# 3. 看内存监控曲线:
#    - 缓慢只涨不跌 → 内存泄漏(堆 或 非堆 或 native)。
#    - 突然飙高 → 某次大请求/大批量加载。
# 4. 容器 OOMKilled 但 -Xmx 已合理 → 查非堆:
#    - Metaspace 涨? 线程数爆炸? 直接内存泄漏(Netty)? native 内存?
#    - 加 -XX:NativeMemoryTracking=summary 看各区用量。

# 经验:
#   - 退出码 137 + 无 Java 异常 → 几乎一定是容器 OOMKilled。
#   - 设了合理 -Xmx 还 OOMKilled → 重点查非堆和泄漏, 别只盯着堆。

# 核心: 先用退出码137/OOMKilled 与 OutOfMemoryError 区分"容器OOM"和"堆OOM";
#   容器 OOM 查总内存(堆+非堆+泄漏), 堆 OOM 查堆设置与对象泄漏。

这套排查路径,让我面对各种"OOM"不再慌乱。最关键的,是分清两种截然不同的 OOM:A. 容器 OOMKilled(本文)——进程总内存超 cgroup limit、被内核杀,现象是退出码 137、Reason: OOMKilled、且没有 Java 异常栈(进程被直接 SIGKILL,来不及打日志);B. JVM 堆 OOM——堆内对象超过 -Xmx、JVM 自己抛 OutOfMemoryError: Java heap space,有完整异常栈排查步骤就此展开:第一,kubectl describe pod 看退出码——137 + OOMKilled 走 A 路线;第二,看应用日志有没有 OutOfMemoryError——有就走 B 路线;第三,看内存曲线——缓慢只涨不跌是泄漏,突然飙高是某次大请求/大批量加载;第四,如果 -Xmx 已合理却 OOMKilled,就重点查非堆(Metaspace 涨没涨?线程数爆没爆?直接内存/native 有没有泄漏?),用 -XX:NativeMemoryTracking=summary 看各区用量。我也总结了经验:退出码 137 + 无 Java 异常,几乎一定是容器 OOMKilled;设了合理 -Xmx 还 OOMKilled,重点查非堆和泄漏,别只盯着堆。归根结底:先用"退出码 137/OOMKilled"和"OutOfMemoryError"区分容器 OOM 与堆 OOM,再分别对症。

下面这张图,是这次 OOMKilled 的诊断与解决全过程:

第四件事:容器退出码与状态的速查表

这次踩坑后,我把容器常见的退出码和状态,整理成一张速查表,下次再遇到"容器又挂了"能快速判断方向。

退出码 / 状态 含义 常见原因
137 + OOMKilled SIGKILL(128+9), 内存超 limit 被内核杀 -Xmx 太大 / 非堆没留余量 / limit 太小 / 泄漏
143 SIGTERM(128+15), 收到停止信号正常退出 滚动更新、缩容、手动停止(正常)
0 进程正常结束 主进程跑完就退(如把一次性任务当常驻服务)
1 / 非0 程序自身异常退出 启动报错、配置错误、依赖连不上
CrashLoopBackOff 反复崩溃, K8s 拉长重启间隔 上述任意原因反复发生(看日志/describe)
OOMKilled(无重启) 被杀但策略不重启 同 137, 检查 restartPolicy

这张表,是我用这次事故换来的"容器急救手册"。最该刻进脑子的,是 137 和 143 的区别:137(SIGKILL)是"被强制杀死"——内存超限被 OOM Killer 干掉,是异常、要排查;而 143(SIGTERM)是"收到了礼貌的停止信号"——滚动更新、缩容时的正常退出,不用慌。其余的也各有指向:退出码 0 却一直重启,往往是把一次性任务当常驻服务跑了(主进程跑完就退);非 0程序自身启动报错(看日志);而 CrashLoopBackOff 不是原因、而是现象——它表示"反复崩溃,K8s 在拉长重启间隔",真正的原因,还得回到 describe日志里去找。它给我的最大启发是:容器挂了别瞎猜,先看退出码——它是定位问题方向的第一把钥匙。

第五件事:容器资源配置的常见误区

顺着这次的根因,我把团队 K8s 资源配置里那些"看着没问题、其实埋雷"的做法,系统排查了一遍。

误区做法 埋下的雷 正确姿势
JVM 不设 -Xmx, 也不开容器感知 按宿主机内存设堆, 必 OOMKilled UseContainerSupport 或显式 -Xmx
-Xmx 设成 = 容器 limit 没给非堆留余量, 仍会超 -Xmx ≈ limit * 0.7~0.75
requests 远小于 limit 超卖严重, 节点内存紧张时被驱逐 requests 贴近真实用量, 别画太大饼
不设 limit(无限制) 一个服务泄漏拖垮整个节点 务必设 limit, 隔离爆炸半径
limit 拍脑袋设, 不看监控 设太小频繁 OOM, 太大浪费资源 按实际峰值用量 + 余量设定
CPU limit 设太小 被 CPU throttle, GC/请求变慢卡顿 CPU 给足, 关注 throttling 指标

这张表,让我对"配置资源"这件事,有了远超"填两个数字"的敬畏。最核心的两条,都和这次的根因一脉相承:其一,JVM 必须感知容器(否则按宿主机设堆,必挂);其二,-Xmx 不能等于 limit,必须给非堆留 25~30% 的余量。而其余几条,是我顺藤摸瓜发现的更大隐患:不设 limit,等于让一个服务的内存泄漏,有能力拖垮整个节点上的所有服务(limit 的意义,正是隔离爆炸半径);requests 画太小的饼,会导致节点超卖,内存紧张时你的 Pod 被驱逐;limit 拍脑袋设,小了频繁 OOM、大了浪费资源,正确做法是看监控、按真实峰值用量加余量来定;甚至 CPU limit 设太小,也会让进程被 throttle,导致 GC 和请求莫名卡顿它们共同指向一个道理:资源配置不是"随便填个数",而是要基于"对程序真实资源消耗的理解"和"线上监控数据"来精算的工程决策——拍脑袋设的每一个数字,都是一颗待引爆的雷。

第六件事:容器化一个服务时,我现在会怎么决策

现在,每当我准备把一个服务容器化、配置它的资源,脑子里都会过一遍这张决策图——核心就一句:让程序感知边界,给资源留余量

这张图的灵魂,是把容器化从"打个镜像跑起来",升级为"让程序与它的资源边界正确对齐"。第一步,看程序是不是 JVM 这类有"自适应内存"的运行时:是 JVM,就再看 JDK 版本——JDK 10+ 容器感知默认开,设好 MaxRAMPercentage=75 即可;JDK 8,就显式 -Xmx 或加 UseContainerSupport;普通进程,则评估它的真实内存峰值第二步:无论哪种,都给非堆和系统留出 25% 以上的余量第三步:limit(峰值+余量)和 requests(贴近真实用量)。最后,也是我以前最缺的一步:进容器验证 MaxHeapSize 真的生效了,再压测、看内存曲线,确认稳定、不 OOMKilled,才敢上线——而不是"部署上去,跑着没崩就以为没事"。

我立下的几条规矩

这场"上 K8s 就反复 OOMKilled"的事故,换来了我做容器化部署时,刻进骨子里的几条铁律:

  1. JVM 进容器,必须让它感知容器内存。JDK10+ 设 MaxRAMPercentage,JDK8 显式 -Xmx 或开 UseContainerSupport,绝不让它按宿主机猜。
  2. -Xmx 永远小于 limit,给非堆留余量。总内存 = 堆 + Metaspace + 线程栈 + 直接内存 + native,-Xmx 取 limit 的 70~75%。
  3. 退出码是第一把钥匙。137=OOMKilled(查内存),143=SIGTERM(正常),0 还重启(任务当服务跑了),非0(启动报错)。
  4. 分清容器 OOM 与堆 OOM。137 无 Java 异常=容器 OOM(查总内存/非堆);有 OutOfMemoryError=堆 OOM(查堆/泄漏)。
  5. limit 必设,按监控定,别拍脑袋。不设 limit 会让一个泄漏拖垮整个节点;数值按真实峰值+余量。
  6. 改完要进容器验证。java -XX:+PrintFlagsFinal 看 MaxHeapSize 是否真的等于预期,别活在"我配了"的错觉里。
  7. 容器不是虚拟机。它是被 cgroup 限制的进程,永远要让程序知道自己的资源边界在哪。

附:一份容器化 Java 服务的内存自查清单

为了让团队不再重蹈覆辙,我把这次的教训,固化成一份"上线前必过"的内存自查清单和命令,贴在这里:

# ===== 容器化 Java 服务内存自查清单(上线前逐项确认)=====

# 1. JVM 是否感知到了容器内存?(进容器执行)
java -XX:+PrintFlagsFinal -version | grep -E "MaxHeapSize|MaxRAMPercentage"
#   ✓ MaxHeapSize 应约等于 (limit * 你设的百分比), 而不是宿主机的 1/4

# 2. 看容器实际内存 limit(进容器执行, 确认 JVM 读的是这个)
cat /sys/fs/cgroup/memory/memory.limit_in_bytes   # cgroup v1
cat /sys/fs/cgroup/memory.max                      # cgroup v2
#   ✓ 应等于你在 yaml 里设的 limit, 而不是宿主机总内存

# 3. 运行时看 Java 进程各内存区用量(开 NMT)
#    启动加: -XX:NativeMemoryTracking=summary
jcmd  VM.native_memory summary
#   ✓ 看 heap / metaspace / thread / direct 各占多少, 加起来要 < limit

# 4. 实时看容器整体内存用量 vs limit
kubectl top pod             # 看 MEMORY 是否逼近 limit
#   ✓ 稳态应在 limit 的 70~85%, 留出 GC 波动空间

# 5. 压测 + 观察(最关键!)
#    用接近生产的流量压测 10~30 分钟, 盯着:
#    - kubectl top pod: 内存有没有持续上涨(泄漏)?
#    - RESTARTS: 有没有偷偷 +1(OOMKilled)?
#    - GC 日志: 是不是频繁 Full GC(堆太小)?

# 6. 退出码兜底检查
kubectl describe pod  | grep -A3 "Last State"
#   ✓ 不应出现 Reason: OOMKilled / Exit Code: 137

# 核心: 容器化 Java 上线前, 逐项验证 JVM 看到的内存=容器limit、
#   各内存区之和

这份清单,现在是我们团队所有 Java 服务上线前的"过关卡"它的每一条,都直指这次事故的某个环节:第 1、2 条,确认 JVM 真的读到了容器的 limit(而不是宿主机内存)——这是这次悲剧的正面照妖镜;第 3 条,用 NMT 把堆、Metaspace、线程、直接内存各区用量摊开,确认它们加起来真的小于 limit;第 4、5 条,用 kubectl top压测,在真实流量下观察内存曲线有没有泄漏、有没有偷偷重启;第 6 条,最后用 describe 兜底,确认没有 OOMKilled它把我过去那种"配好参数、部署上去、跑着没崩就算成功"的侥幸,彻底替换成了"每一个关键假设,都有对应的命令去验证"的踏实。这,正是这次事故留给我的最宝贵的东西:对于资源这种"有硬上限、超了就死"的东西,永远不要相信"应该没事",而要用一条条命令、一次次压测,把"确实没事"亲手验证出来。

写在最后

回头看,这场由"JVM 看不见容器内存"引发的、反复 OOMKilled 的事故,真正教给我的,是一个比"配置 JVM 参数"本身更深远的道理:当我们把一个旧世界的程序,搬进一个新世界的环境时,最危险的,往往不是那些"明显不兼容"的地方,而是那些"它自以为还和过去一样、其实早已天翻地覆"的隐性假设JVM 的"按可用内存的 1/4 设堆",这个在"独占整台物理机"的旧时代无比合理的默认行为,一旦被搬进"被 cgroup 切割资源"的容器新时代,就瞬间变成了一颗定时炸弹——它还在用旧世界的眼睛(宿主机内存),打量着新世界的笼子(容器 limit),而这,正是悲剧的源头。所以,做容器化、做任何"环境迁移",最需要的,是一种"主动质疑一切隐性假设"的清醒:你要不断地问:"这个程序,它以为自己运行在一个什么样的环境里?这个假设,在新环境下,还成立吗?"真正平滑的迁移,从来不是"把东西搬过去就行",而是逐一审视并校正,那些被程序默默依赖、却在新环境下已然失效的前提让程序"看清"它真实身处的世界——这,是我用一次"退出码 137"的崩溃,换来的、关于容器化最朴素、也最深刻的领悟。如果这篇复盘,能让你在下一次把服务塞进容器之前,多问一句"它知道自己只有这么点内存吗",那我对着 OOMKilled 熬的这大半天,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我的服务调外部接口一到高峰就报"cannot assign requested address"、机器上堆了几万个 TIME_WAIT 连接,我盯着 netstat 排查了大半天才发现连接根本没复用

2026-6-2 1:43:46

技术教程

我把大模型当成一个稳定的函数写进了自动化流程,结果同样的输入每次跑出的结果都不一样、测试时灵时不灵,我对着这种飘忽不定排查了大半天的复盘

2026-6-2 1:55:30

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索