Administrator
发布于 2024-04-16 / 6847 阅读
98

容器环境下 JVM 内存配置的正确姿势

一个服务在 K8s 里跑得好好的,突然被重启,看 Pod 事件只有一行 OOMKilled。我第一反应是"堆不够了",加了 -Xmx 之后反而重启更频繁。后来才明白,我搞混了两种 OOM。

两个 OOM 不是一回事

JVM 的堆 OOM(java.lang.OutOfMemoryError: Java heap space)是 JVM 自己抛的,进程还在,可以触发 dump、留日志。而 K8s 的 OOMKilled 是 Linux 内核的 cgroup 内存上限触发的,直接发 SIGKILL,进程瞬间没了,连 GC 日志都来不及写。我们的服务就是后者——容器总内存超了 limit,被内核杀掉,跟堆大小其实没直接关系。

容器里 JVM 看不见真实内存

旧版 JDK 在容器里默认读的是宿主机的物理内存,而不是 cgroup 的 limit。比如宿主机 64G,容器 limit 是 2G,JVM 按 64G 去算堆,直接把容器撑爆。JDK 8u191+ 和 JDK 11+ 才默认开启 UseContainerSupport,认 cgroup。我们确认过:

java -XX:+PrintFlagsFinal -version 2>/dev/null | grep -i containersupport
# bool UseContainerSupport = true

用 MaxRAMPercentage 代替写死 Xmx

以前我们写死 -Xmx1536m,容器 limit 一改就得跟着改,经常忘。换成按百分比:

-XX:MaxRAMPercentage=75.0
-XX:InitialRAMPercentage=50.0
-XX:MinRAMPercentage=50.0

含义是:堆最多用容器 limit 的 75%。limit 2G 时堆约 1.5G,剩下 512M 留给元空间、线程栈、直接内存和 OS。注意 MaxRAMPercentage 只控制堆,非堆部分(尤其是堆外直接内存 -XX:MaxDirectMemorySize 和线程栈)不在它之内,这部分超了照样 OOMKilled。

一次真实的配比

分配说明
容器 limit2GiK8s requests/limits
堆 MaxRAMPercentage=75%约 1.5Gi年轻代+老年代
元空间 MaxMetaspaceSize256m防类加载泄漏
直接内存 MaxDirectMemorySize128mNIO/Netty 用
线程栈 ThreadStackSize×线程数约 120m虚拟线程后大幅降

加起来约 2G,留了 0 缓冲——后来我们把 MaxRAMPercentage 降到 70%,给 OS 和页缓存腾出 100M 余量,OOMKilled 消失。

怎么确认是哪种 OOM

# 看 Pod 事件
kubectl describe pod xxx | grep -A3 -i oom
# 看是否被 cgroup 杀
dmesg | grep -i "killed process"
# JVM 侧是否留了堆 dump
ls -lh /path/to/*hprof

有 hprof 文件的是堆 OOM;只有 OOMKilled 事件、无 dump 的,是 cgroup 层面的总内存超限。诊断方向完全不同:前者调 GC/找内存泄漏,后者调容器 limit 或非堆参数。

小结

容器里的 JVM 内存配置,核心是"让堆占比 + 非堆 + OS"不超过 limit。写死 -Xmx 容易和 limit 脱节,用 MaxRAMPercentage 更稳,但别忘了直接内存和线程栈这两块堆外大户。看到 OOMKilled 别急着加堆,先分清是内核杀的还是 JVM 抛的,方向错了越调越坏。JDK 21 上虚拟线程把线程栈开销压下来后,这层预算更好算,但直接内存该留还得留。

参考