本文摘要一个 Spring Boot 文件服务用 docker run -m 2g --memory-swap 2g 部署JVM 参数写的是 -Xmx2g。一、问题与结论压测持续到堆用量打印 1450 MB进程被 SIGKILL退出码 137登录宿主机执行free -havailable列显示 48 GB。第一反应是宿主机有内存Docker 却杀了进程排查方向就此被带偏。结论是两套计数口径不同-m写入的是 cgroup 硬限制计数覆盖匿名页与 page cache宿主机free把 page cache 归入buff/cache视为可回收内存。-Xmx只约束堆上限与-m等值时 Metaspace、线程栈、Code cache 没有任何余量几乎必然提前触发 cgroup 层的 OOM。二、排查与选择依据先判断 OOM 发生在哪一层。docker inspect返回OOMKilled: true、dmesg出现Memory cgroup out of memory说明触发点在 cgroup 层堆内的OutOfMemoryError没有机会抛出进程直接被 SIGKILL。Java 堆 OOM 的特征是退出码非 137、日志出现java.lang.OutOfMemoryError两者不要混淆。cgroup v2 下进一步看计数构成cat/sys/fs/cgroup/memory.events# oom_kill 计数cat/sys/fs/cgroup/memory.stat|grep-E^(anon|file)# anon 与 file 两类计数file非零说明 page cache 已经计入限额。监控指标应改为采集memory.currentcgroup v1 对应memory.usage_in_bytes宿主机free的available不反映容器剩余额度。再看-Xmx与-XX:MaxRAMPercentage的口径差异。-Xmx是绝对字节数只控制堆上限非堆部分依旧占用同一批 cgroup 额度-XX:MaxRAMPercentage读取容器可见内存cgroupmemory.max按百分比折算堆上限JDK 11 的容器感知会自动读取并折算无需把限额写进启动参数。两者二选一不要叠加。替代方案与取舍方案选择条件代价不该用的场景-XX:MaxRAMPercentage75通用后端可接受堆缩 25%同限额下堆上限变小堆值已按业务精确固定必须绝对字节放开--memory-swap批处理、非延迟敏感换页抖动、尾延迟恶化在线交易、SLA 严格手动改 cgroupmemory.highcgroup v2可直接挂载 sysfs脱离 Docker 参数运维复杂托管编排平台无 cgroup 挂载权限若堆值已按业务精确固定直接下调-Xmx给非堆留空间不要用MaxRAMPercentage覆盖既有-Xmx。三、关键原理cgroup v2 的memory.current统计该 cgroup 下所有进程的匿名页、文件页缓存以及部分内核内存宿主机free的available把可回收的 page cache 算作可用。同一时刻两个计数器可以分别显示 2 GB 与 48 GB这不是统计错误而是统计范围不同——前者是容器视角后者是全局视角。JVM 进程对memory.current的贡献可拆为cgroup 计数 ≈ 堆(-Xmx) Metaspace 线程栈 Code cache Direct buffers GC 工作区 page cache按-Xmx2g常见非堆合计 200 到 400 MB文件读写再叠 page cache总量即超 2 GB。-XX:MaxRAMPercentage75把堆上限定在约 1.5 GB为非堆与缓存预留 500 MB堆满时走OutOfMemoryError退出路径而非 SIGKILL便于从日志定位问题。四、可运行示例环境Linux 5.15、Docker 24、cgroup v2stat -fc %T /sys/fs/cgroup/输出cgroup2fs。镜像基于eclipse-temurin:17-jdk入口命令为java。DockerfileFROM eclipse-temurin:17-jdk WORKDIR /app COPY OomTest.java /app/ RUN javac OomTest.java ENTRYPOINT [java]OomTest.javaimportjava.util.ArrayList;importjava.util.List;publicclassOomTest{publicstaticvoidmain(String[]a)throwsException{Listbyte[]lnewArrayList();for(inti0;;i){l.add(newbyte[50*1024*1024]);System.out.printf(heap%d MB, cgroup%d MB%n,(i1)*50,read());Thread.sleep(200);}}staticlongread(){for(Stringp:newString[]{/sys/fs/cgroup/memory.current,/sys/fs/cgroup/memory/memory.usage_in_bytes}){try{returnLong.parseLong(java.nio.file.Files.readString(java.nio.file.Paths.get(p)).trim())/1048576;}catch(Exceptionignored){}}return-1;}}失败配置不加--rm便于事后检查退出状态dockerbuild-toom-test.dockerrun--nameoom-run-m2g --memory-swap 2g oom-test-Xmx2gOomTest预期输出heap打印到约 1.4 GB 量级时cgroup已逼近 2 GB差值来自 Metaspace、线程栈、Code cache 与 page cache具体数值随 JDK 发行版而异随后进程被 SIGKILL退出码 137。实际输出在宿主机执行docker inspect --format {{.State.OOMKilled}} {{.State.ExitCode}} oom-run输出true 137即确认 cgroup OOM再执行dmesg | grep -i oom可见Memory cgroup out of memory。若看到的是java.lang.OutOfMemoryError且退出码非 137说明限额未按预期生效检查docker info中Cgroup Driver与内核版本是否匹配。常见失败镜像内 JDK 早于 8u191JVM 按宿主机物理内存设默认堆heap打印远超 2 GB 仍未被杀。修复方法是改用 JDK 11或在-Xmx/MaxRAMPercentage中二选一。清理与修正配置dockerrmoom-rundockerrun--rm-m2g --memory-swap 2g oom-test-XX:MaxRAMPercentage75OomTest此时堆上限约 1.5 GB堆满时抛OutOfMemoryError退出码非 137。五、验证结果与边界按MaxRAMPercentage75留余量适合多数无状态后端堆缩 25% 换来可诊断的退出路径运维能从日志区分 Java 堆 OOM 与 cgroup OOM。三条边界需要单独考虑堆内数据本身贴近 2 GB 时应上调-m而不是调参大文件 IO 服务的 page cache 占比高即便堆有余量也易超限需用O_DIRECT或减小读取块降低缓存压力延迟敏感业务不要用 swap 兜底。参数折算的具体数值受 JDK 发行版影响实施前用java -XX:PrintFlagsFinal -version | grep MaxHeapSize核对实际堆上限。参考资料Linux cgroup-v2 memory controllerdocs.kernel.orgDocker run 参考限制容器内存Docker EngineOpenJDK 17 java 命令参考-XX:MaxRAMPercentage