ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

容器里堆只用 60% 却被 OOMKilled:JVM 少算的内存去哪了

容器里堆只用 60% 却被 OOMKilled:JVM 少算的内存去哪了 本文摘要堆只用六成就被杀掉缺口来自元数据、线程栈与直接内存这些非堆项。用NMT把内存拆成八个类别逐项归因再按总账给各类显式设上限。该路径限JDK17容器环境NMT常开多占内存内存贴着限额的服务须谨慎。一、问题与结论kubectl describe pod给出OOMKilledjstat -gc里used约 720MB-Xmx1200m只用到六成Pod 重启后两小时又被杀。差别在口径cgroup 的limits.memory统计的是进程 RSS堆只是子项Metaspace、线程栈、Code Cache 与 Direct Buffer 合计约 800MB 就足以越过 2Gi。排查用-XX:NativeMemoryTracking逐类归因治理是给 Non-Heap 显式设上限并留余量。触发者与业务代码无关堆外分配偏多的服务都会复现常见的是 Netty/gRPC 的零拷贝缓冲区与规模偏大的线程池。二、排查与选择依据两个口径要分清容器侧看docker stats的USAGE与/proc/1/status的VmRSSJVM 侧看GC.heap_info的used差值就是待解释的部分。诊断顺序docker stats --no-stream my-oom-app看总占用与 limitdocker exec my-oom-app cat /proc/1/status | grep VmRSS拿到 RSSdocker exec my-oom-app jcmd 1 VM.native_memory summary拆出八类占用jcmd 1 VM.native_memory baseline建基线隔一段时间用VM.native_memory detail.diff看增量是否回落。判断标准Java Heap的 used 稳定而Internal或Class持续增长且不回落属于 Direct Buffer 泄漏或元数据膨胀Netty 异常路径漏 release、CGLIB 热部署旧类未释放都归此类各类都稳定只是总量偏大属预算不合理改启动参数即可不必动业务代码。替代方案与取舍做法选择条件代价不该用的情况NMT summary 常开定期detail.diff需要逐类归因能改启动参数summary 约增加 5%–10% RSS内存贴着 limit没有余量吃开销显式设MaxMetaspaceSize等上限MaxRAMPercentage60留白只求稳定运行堆利用率偏低上限过紧会抛OutOfMemoryError: Metaspace堆峰值波动大无法预估各类上限JFR 采集jdk.NativeMemoryUsage已有 JDK 11 与分析流程采集与事后分析成本高只想判断是否越限的短命任务K8scontainer_memory_working_set_bytes告警拿不到 JVM 内部指标只能预警不能定位已知根因、要量化修复收益上线不到一周就OOMKilled时先调limits.memory与-Xmx的比例通常更快收敛。三、关键原理NMT 把 JVM native memory 划成八类堆只是第一项NMT 类别内容典型量级Java Heap对象存储-Xmx的 60% 左右ClassMetaspace Compressed Class Space数十到数百 MBThread线程栈线程数 ×-XssCodeJIT Code Cache24–240 MBGC标记位图等 GC 数据结构堆的 5%–20%Internal/Symbol内部结构、符号表数十 MBOtherDirect Buffer、JNI、Unsafe不透明最容易失控三个机制决定预算写法。其一cgroup 记的是 RSS-Xmx并未为其余类别预留空间。其二-XX:UseContainerSupport在 JDK 10 默认开启按容器 limit 修正-XX:MaxRAMPercentage与处理器数量但不会自动收紧 Metaspace、Code Cache 与线程栈。其三-XX:MaxDirectMemorySize默认跟随-XmxDirect Buffer 的回收依赖 Cleaner 与 GC 触发释放时机不受调用方控制。总账按“堆 Metaspace Code Cache 线程数 ×-Xss Direct Buffer GC 开销 NMT 自身开销 ≤limits.memory”来写。四、可运行示例环境为 Docker 与eclipse-temurin:17-jdk镜像cgroup v2 环境同样适用容器内存限额 2GB。先构建镜像再带限额运行最后执行诊断。MemoryEater.java模拟堆内 720MB、Direct Buffer 300MB 与 200 个阻塞线程importjava.nio.ByteBuffer;importjava.util.ArrayList;importjava.util.List;importjava.util.concurrent.CountDownLatch;publicclassMemoryEater{publicstaticvoidmain(String[]args)throwsException{Listbyte[]heapnewArrayList();for(inti0;i720;i){heap.add(newbyte[1024*1024]);// 堆内约 720MB}ListByteBufferdirectnewArrayList();for(inti0;i300;i){direct.add(ByteBuffer.allocateDirect(1024*1024));// 堆外 300MB}CountDownLatchlatchnewCountDownLatch(1);for(inti0;i200;i){// 200 个阻塞线程栈默认 512KB–1MBnewThread(()-{try{latch.await();}catch(InterruptedExceptionignored){}}).start();}System.out.println(ready pidProcessHandle.current().pid());Thread.sleep(Long.MAX_VALUE);}}Dockerfile为各类 Non-Heap 显式设上限并开启 NMTFROM eclipse-temurin:17-jdk WORKDIR /app COPY MemoryEater.java . RUN javac MemoryEater.java # MaxDirectMemorySize 留出余量避免恰好贴 300MB 上限抛 OutOfMemoryError: Direct buffer memory ENTRYPOINT [java,-Xmx1200m,-XX:MaxMetaspaceSize128m,-XX:ReservedCodeCacheSize128m,-XX:MaxDirectMemorySize512m,-XX:NativeMemoryTrackingsummary,-XX:UseContainerSupport,MemoryEater]构建、运行与诊断dockerbuild-toom-diagnosis.dockerrun-d--namemy-oom-app--memory2g oom-diagnosissleep5dockerstats --no-stream my-oom-appdockerexecmy-oom-appcat/proc/1/status|grepVmRSSdockerexecmy-oom-app jcmd1VM.native_memory summarydockerexecmy-oom-app jcmd1GC.heap_infodockerexecmy-oom-appcat/sys/fs/cgroup/memory.max2/dev/null\||dockerexecmy-oom-appcat/sys/fs/cgroup/memory/memory.limit_in_bytes预期输出Java Heapused 约 720MBThread约 200MBInternal/Other约 300MBClassCodeGC约 300MBcommitted 合计约 2GB叠加 NMT 开销后VmRSS贴近 2Gi随后出现OOMKilled。上述数值为未验证示意值。实际输出在终端比对VmRSS与GC.heap_info的 used 差值并确认memory.max为2147483648若差值接近 NMT 各类之和即可确认根因是 Non-Heap 越限。失败处理启动参数里漏掉-XX:NativeMemoryTrackingsummary时jcmd 1 VM.native_memory summary会返回未开启追踪的提示。NMT 只能在 JVM 启动时开启补进ENTRYPOINT后重启 Pod或改用 JFR 事后采样。五、验证结果与边界验证分两步先用示例把容器打到 limit 并拿到OOMKilled事件再逐项设上限重跑观察同一时段detail.diff是否收敛。文中内存分布数字均为未验证示意值。边界四条。NMT 有开销summary 约 5%–10%、detail 约 10%–20% 额外 RSS贴限服务不要长期开 detail。Other里的 Direct Buffer 释放时机不可控-XX:MaxDirectMemorySize只挡越界不保证回收。cgroup v2 需 JDK 15 完整支持JDK 8/11 的 update 版本需单独确认。虚拟线程JDK 21不占平台线程栈Thread一类会明显变小仍要为 pinning 场景预留平台线程预算。参考资料java 命令手册JDK 17jcmd 命令手册JDK 17
返回列表