CgroupV2 Java内存OOM问题分析

aws 从kubernetes 1.33 开始将不再提供 AL2的节点镜像,如果要使用 1.33 必须要升级操作系统镜像到 AL2023,但很多用户发现升级到1.33 & AL2023 后,以前跑的好好的 java 程序,经常会出现 OOM ,本文将详细分析其原因,以及解决方法/原理
一、问题分析
cgroup v2 的架构革新
cgroup v2 重构了资源控制模型,将 v1 的分散子系统(如memory
、cpu
)整合为单一层级树,并调整了关键文件路径(如内存限制从/sys/fs/cgroup/memory/memory.limit_in_bytes
改为/sys/fs/cgroup/memory.max
)。
详见:https://github.com/dask/distributed/issues/6894 问题根源:早期 Java 11 版本(低于 11.0.16)的容器资源检测逻辑仅兼容 cgroup v1 路径,无法解析 v2 的新接口,导致 JVM 误判可用内存。JVM 的内存识别机制失效
- 当 Java 11 < 11.0.16 在 cgroup v2 环境下启动时:
- JVM 仍尝试从 v1 路径读取内存限制,但该路径不存在或返回空值。
- 作为回退机制,JVM 转而读取 宿主机物理内存(例如 16GB),而非容器限制(例如 1GB)。
- 结果:JVM 根据宿主机内存计算堆大小(默认
MaxHeapSize = 物理内存 × 25%
),导致堆内存远超容器限制(如 4GB vs 1GB)。
- 当 Java 11 < 11.0.16 在 cgroup v2 环境下启动时:
AL2023 的默认配置变更
Amazon Linux 2023(AL2023)弃用 cgroup v1,强制启用 cgroup v2 作为唯一控制组系统。相较之下,AL2 默认使用 cgroup v1。而EKS 从1.33 开始将不再提供 AL2的镜像
后果:在 AL2023 节点上运行的旧版 Java 11 必然触发上述内存识别错误。文件描述符限制的叠加影响
AL2023 的 systemd 升级将默认文件描述符限制设为infinity
(实际值 ≈9e18),而 AL2 为810042
。- Java 的
-XX:-MaxFDLimit
参数(默认会打开)会尝试提升 FD 限制至最大值; - 部分 JVM 内部数据结构(如哈希表)的大小与 FD 数量正相关;
- 超高 FD 限制导致 JVM 额外分配大量非堆内存,进一步挤占容器内存配额
- Java 的
graph LR A[JVM 启用 MaxFDLimit] --> B[尝试提升 FD 至“无限”] B --> C[分配 FD 管理结构
(哈希表/线程栈等)] C --> D[非堆内存占用激增] D --> E[容器总内存超限] E --> F[Linux OOM Killer 终止进程]
OOM流程分析
graph TD A[Java 11 < 11.0.16 启动] --> B[读取 cgroup v1 路径失败] B --> C[回退读取宿主机内存
(如 16GB)] C --> D[按 25% 计算堆大小
(MaxHeapSize=4GB)] D --> E[实际堆分配超出容器限制] E --> F[JVM 申请内存超限] F --> G[Linux OOM Killer 终止进程
(Exit Code 137)]
关键环节:
- JVM 未感知容器限制,按宿主机资源分配堆内存 → 堆内存溢出;
- FD 限制激增 → 非堆内存(元空间/线程栈)膨胀;
- cgroup v2 的 OOM Killer 更激进,一旦容器总内存(堆+非堆+JVM 自身)超限立即终止进程(cgroupv1 可以通过设置:memory.soft_limit_in_bytes 延缓OOM)
二、解决方案
解决方案有两种,根本的方案还是升级jdk版本,还有的则可以通过增加参数绕过去 先确认你的jvm是否支持cgroupv2
java -XshowSettings:system -version
- 支持时输出:
Provider: cgroupv2
- 不支持时输出:
No metrics available for this platform
2.1 无法升级jdk场景
无法升级的话可以通过参数控制
-Xmx512m -Xms512m # 强制堆 ≤ 容器限制的 70%:cite[7]:cite[10]
-XX:-MaxFDLimit # 避免 FD 相关内存膨胀:cite[1]:cite[4]
-XX:ActiveProcessorCount=2 # 防止 GC 线程过多挤占内存:cite[10]
修改 containerd 配置,限制 FD 默认值(例:LimitNOFILE=1024:65536)
2.2 升级JDK
参考kubernetes官网:https://kubernetes.io/docs/concepts/architecture/cgroups/#migrating-cgroupv2 java8/java11.0.16以前的版本,java15 都需要升级
To migrate to cgroup v2, ensure that you meet the [requirements](https://kubernetes.io/docs/concepts/architecture/cgroups/#requirements), then upgrade to a kernel version that enables cgroup v2 by default.
The kubelet automatically detects that the OS is running on cgroup v2 and performs accordingly with no additional configuration required.
There should not be any noticeable difference in the user experience when switching to cgroup v2, unless users are accessing the cgroup file system directly, either on the node or from within the containers.
cgroup v2 uses a different API than cgroup v1, so if there are any applications that directly access the cgroup file system, they need to be updated to newer versions that support cgroup v2. For example:
- Some third-party monitoring and security agents may depend on the cgroup filesystem. Update these agents to versions that support cgroup v2.
- If you run [cAdvisor](https://github.com/google/cadvisor) as a stand-alone DaemonSet for monitoring pods and containers, update it to v0.43.0 or later.
- If you deploy Java applications, prefer to use versions which fully support cgroup v2:
- [OpenJDK / HotSpot](https://bugs.openjdk.org/browse/JDK-8230305): jdk8u372, 11.0.16, 15 and later
- [IBM Semeru Runtimes](https://www.ibm.com/support/pages/apar/IJ46681): 8.0.382.0, 11.0.20.0, 17.0.8.0, and later
- [IBM Java](https://www.ibm.com/support/pages/apar/IJ46681): 8.0.8.6 and later
- If you are using the [uber-go/automaxprocs](https://github.com/uber-go/automaxprocs) package, make sure the version you use is v1.5.1 or higher.
2.3 总结
综上,还是更建议升级,java11 优先考虑小版本升级。
但在有问题的jdk版本,我们很多时候已经设置了 -Xmx2048m -Xms2048m
已经限制了堆内存,还会命中吗?
-Xmx
仅限制堆内存,但 JVM 总内存包含:
- 堆内存(Heap):由
-Xmx
控制(您已配置 2GB) - 非堆内存(Non-Heap):
- 元空间(Metaspace):类元数据,默认无上限
- 线程栈(Thread Stacks):每线程约 1MB
- JIT 编译缓存
- 直接内存(Direct Memory):NIO Buffer 等
- JVM 自身开销:GC 数据结构等
graph LR A[容器内存限制 4GB] --> B[JVM 堆内存 2GB] A --> C[非堆内存
元空间/线程栈/JIT缓存] A --> D[直接内存
NIO Buffer/JNI] A --> E[JVM 自身开销
GC数据结构等] C -->|默认无上限| F[可能突破容器限制] D -->|默认无上限| F
完整命令可以参考:
java \
-Xms2048m -Xmx2048m \ # 控制堆内存
-XX:MaxMetaspaceSize=256m \ # 限制元空间上限
-XX:MaxDirectMemorySize=512m \ # 限制直接内存
-XX:-MaxFDLimit \ # 禁用 FD 限制提升
-XX:ActiveProcessorCount=$(nproc) \ # 精确匹配容器 CPU
-XX:NativeMemoryTracking=detail \ # 开启内存跟踪
-XX:+UnlockDiagnosticVMOptions \
-XX:+PrintContainerInfo \ # 输出容器资源识别情况
-jar your_app.jar