JVM 容器内存限制(cgroup 感知)
更新时间:2026-09-18。容器里 JVM 最常见的崩溃不是 Java OOM,而是进程总内存超过 K8s/Docker limit 被内核 OOMKilled。本篇讲怎么配。
这个问题的根因
老版本 JDK(8u131 之前)只看宿主机物理内存,不看 cgroup 限额。容器只给了 1GB,JVM 却按宿主机 64GB 算默认堆,直接超限被杀。即使新版已感知 cgroup,另一类坑仍在:容器限额约束的是进程总内存,而 -Xmx 只管堆。
容器 limit ≥ 堆 Xmx
+ Metaspace
+ 线程栈(线程数 × -Xss,常被低估)
+ 直接内存 DirectByteBuffer(-XX:MaxDirectMemorySize)
+ CodeCache / JIT / GC 结构 / JVM 自身只把 -Xmx 设成容器 limit,堆外部分必然让总内存越界。
配置:MaxRAMPercentage
JDK 8u191+ / JDK 10+ 已默认开启容器感知(-XX:+UseContainerSupport),推荐用百分比而非写死堆大小:
# 容器 limit 2GB,堆最多用 50%(约 1GB),其余留给堆外
ENV JAVA_OPTS="-XX:MaxRAMPercentage=50.0 -XX:InitialRAMPercentage=50.0"经验起点:普通服务 50% 左右;线程多(大线程池)或大量 NIO 直接内存时降到 40%~25%,给栈和 DirectMemory 留足。简单应用可到 70%,但不要把堆顶到 limit。
# 也可继续用 -Xmx,但要自己保证 堆 + 堆外 < limit
-Xmx1g -XX:MaxDirectMemorySize=256m -XX:MaxMetaspaceSize=256m -Xss512k验证 JVM 看到的限额
# 容器内确认 JVM 识别到的是 cgroup 值而不是宿主机
java -XX:+PrintFlagsFinal -version | grep -Ei 'maxheap|maxram|usecontainersupport'
jcmd <pid> VM.flags | grep -i ram
jinfo -flag MaxHeapSize <pid>
# 打开 NMT 看堆外各类内存占用
-XX:NativeMemoryTracking=summary
jcmd <pid> VM.native_memory summary scale=MB观察容器实际用量:kubectl top pod、docker stats,再对照进程 RSS(ps -o rss),看堆外是否在逼近 limit。
常见坑
-Xmx= 容器 limit:忽略 Metaspace、线程栈、直接内存,必被 OOMKilled;堆只取限额的一部分。- JDK 太老不感知 cgroup:8u131 前需
-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap,最稳妥是升级到 8u191+/11+。 - 线程数 × 栈被低估:几百上千线程 ×1MB 栈就是几百 MB 堆外,可适当下调
-Xss。 - 看到
java.lang.OutOfMemoryError就只加堆:先分清是堆内 OOM(加 Xmx/查泄漏,见 jmap 实战)还是容器 OOMKilled(看退出码 137 / dmesg),后者要压总内存而非加堆。 - limit 与 request 差太大:调度与实际使用脱节,建议关键服务 limit≈request。
总结
容器的 memory limit 约束的是 JVM 进程总内存,而 -Xmx 只管堆,总占用还要加上 Metaspace、每线程栈、直接内存和 JIT/GC 开销,因此旧版 JDK 不感知 cgroup 会按宿主机内存设堆而被秒杀,新版虽默认 UseContainerSupport 也必须把堆设成限额的一部分——推荐 8u191+/11+ 用 -XX:MaxRAMPercentage(普通服务约 50%、线程多或直接内存大时降到 40%~25%),用 jcmd VM.flags 确认识别到的是 cgroup 限额、用 Native Memory Tracking 看堆外,并通过退出码 137 区分容器 OOMKilled 与堆内 Java OOM,后者才需要加堆或查泄漏。