来源:蹦跶的小羊 | 发布日期:2026-05-21
JVM 容器 OOM 排障指南 — 堆外内存视角
JVM 堆只用了 70%,容器却被 OOMKilled?核心原因是容器(cgroup)看到的是进程所有 RSS,而监控面板通常只展示 JVM 堆内存。堆外内存(DirectByteBuffer、Metaspace、线程栈等)悄悄吃满额度才是元凶。
核心问题:两个内存视角的不对称
Java 服务在容器中频繁 OOMKilled,但 JVM 堆内存利用率仅 70% 左右、看似充裕,这是最常见的「容器 OOM 但堆没问题」故障模型。
JVM 眼里的内存
JVM 进程内存
├── Heap(堆) ← -Xmx 控制,GC 管理
│ ├── Young Generation
│ └── Old Generation
├── Metaspace ← 类元数据,-XX:MaxMetaspaceSize 控制
├── Code Cache ← JIT 编译后的本地代码
├── Thread Stacks ← 每个线程一份,-Xss 控制单线程大小
├── Direct Memory ← NIO 的 DirectByteBuffer,-XX:MaxDirectMemorySize 控制
├── GC 自身的元数据 ← 卡表、记忆集、forwarding table 等
├── JNI / Native 分配 ← 调用 native 库时的分配
└── Symbol Table / String Table 等
监控面板上看到的「JVM 堆 70%」只是 Heap 这一格。其余部分默认不展示,但都实实在在地占着进程内存。
容器(cgroup)眼里的内存
cgroup 统计的是整个 cgroup 下所有进程的所有内存映射,它不知道什么叫堆、什么叫 Metaspace:
cgroup.memory.current = RSS(常驻物理内存)
+ Page Cache(文件读写缓存,会计入!)
+ Kernel Memory(slab、socket buffer 等)
两视角叠加
容器 limits: 2Gi
└── cgroup 看到的 RSS
└── JVM 进程 RSS = 1.4G(堆,70% 利用率)
+ 300M(Metaspace + CodeCache)
+ 400M(DirectByteBuffer)
+ 100M(线程栈,500 线程 × 256K)
+ 其他杂项
──────────────
≈ 2.2G ← 超过 2Gi,OOMKilled
堆外内存四大「暗坑」
坑一:DirectByteBuffer(NIO 直接内存)
出现场景: Netty、gRPC、Kafka client、RocketMQ client、任何基于 NIO 的网络框架。
释放机制陷阱: DirectByteBuffer 的物理内存释放依赖于对应 Java 对象被 GC 回收时触发 Cleaner 去执行 free。隐含两个前提:
- 必须有足够的 GC 压力 — 堆大且 Full GC 少触发时,已用完的 DirectByteBuffer 对象长期挂在老年代,堆外内存就不释放
- Full GC 不能太慢 — 即使触发了,从
Cleaner入队到真正执行free之间有时间窗口
排查命令:
# 查看 DirectBuffer 当前占用量
jcmd <PID> VM.native_memory summary | grep "Internal"
# 或通过 JMX
jconsole 或 JMC → Memory → BufferPool -> DirectBuffer
建议方案:
- 显式设置
-XX:MaxDirectMemorySize(默认等于-Xmx,非常危险) - 应用层 ByteBuffer 池化(Netty 的
PooledByteBufAllocator就是为此设计) - 监控 DirectBuffer 总量并设告警
坑二:Metaspace 持续膨胀
出现场景: 动态类加载(CGLIB 代理、反射生成、Groovy 脚本、GraalVM 多语言)、热部署、多租户框架。
特点: Metaspace 的增长通常是单向的——JVM 几乎不主动回收已加载但不再使用的类元数据。
排查命令:
# 查看 Metaspace 使用量
jstat -gc <PID> | awk '{print $1, $9, $10}'
# 或 NMT
jcmd <PID> VM.native_memory summary | grep "Metaspace"
建议方案:
- 显式设置
-XX:MaxMetaspaceSize(推荐 256M-512M) - 排查类加载泄漏:
-XX:+TraceClassLoading -XX:+TraceClassUnloading
坑三:线程栈累积
出现场景: 线程池过大、线程泄漏(未捕获异常导致线程重创但旧线程未释放)、阻塞 I/O 导致线程堆积。
占用计算: 每线程栈默认 1M(Linux x64)或 256K(容器内用 -Xss256k)。500 个线程 × 256K = 128M。如果没用 -Xss 缩小,500 × 1M = 500M。
排查命令:
# 查看线程数
jstack <PID> | wc -l
top -H -p <PID> | wc -l
# 缩小线程栈后必须额外设置内核限制
ulimit -s 256
坑四:GC 元数据 + Native 分配
包括: GC 的卡表(Card Table)、记忆集(Remembered Set)、GC forwarding table;以及 glibc 分配器内部碎片(jemalloc 可改善)。
排查命令:
# NMT 大类
jcmd <PID> VM.native_memory summary scale=MB
# 重点关注 Internal + Other 类别
排障工具箱
NMT(Native Memory Tracking)
Java 8u40+ / Java 9+ 内置,最核心的堆外内存诊断工具:
# 启动时开启(会产生 ~5% 性能开销)
-XX:NativeMemoryTracking=summary # 或 detail
# 运行时查看
jcmd <PID> VM.native_memory summary
jcmd <PID> VM.native_memory summary scale=MB
# 做 baseline 对比(定位泄漏)
jcmd <PID> VM.native_memory baseline
# 过一段时间后
jcmd <PID> VM.native_memory summary.diff
cgroup 内存统计
# cgroup v2(主流)
cat /sys/fs/cgroup/memory.current # 当前用量
cat /sys/fs/cgroup/memory.max # 上限
cat /sys/fs/cgroup/memory.stat # 详细分类(page cache 等)
# 查看进程实际 RSS
cat /proc/1/status | grep VmRSS
其他辅助
# 查看系统 OOM 记录
dmesg | grep -i "out of memory" | tail -10
# 查看容器 Exit Code
kubectl describe pod <pod> | grep -E "State|Exit|OOM"
最佳实践:参数配置清单
# 推荐的 Java 容器启动参数(Java 10+)
JAVA_OPTS="
-Xmx <容器 limit 的 60-70%> # 比传统 75-80% 更保守,留空间给堆外
-XX:MaxMetaspaceSize=256m
-XX:MaxDirectMemorySize=256m
-Xss256k # 减少线程栈占用
-XX:+UseContainerSupport # Java 10+ 默认开启,确认即可
-XX:NativeMemoryTracking=summary # 生产可开启,开销 ~5%
-XX:+PrintClassHistogram # 可选
"
堆外总预算估算公式:
堆外预算 = 容器 memory.limit - Xmx
- 安全余量(建议 10-15% 的 limit)
典型分布(2Gi 容器, -Xmx1200m):
堆外预算 ≈ 800M
├─ Metaspace: ∼200M
├─ Direct Memory: ∼200M
├─ Thread Stacks: ∼100M (400 线程 × 256K)
├─ Code Cache + GC: ∼100M
└─ glibc 碎片 + 余量: ∼200M
关联页面
| 页面 | 关联点 |
|---|---|
| docker-production-pitfalls | Docker 容器 OOMKilled 排查(坑四) |
| k8s-resource-limits-configuration | K8s 资源限制与 QoS / oom_score_adj 机制 |
| linux-memory-management-deep-dive | Linux 内存管理(OOM Killer 三级回收) |
| server-performance-four-dimensions | 服务器性能排查四维法(含 Java 内存提示) |
| linux-api-performance-tuning-case-study | JVM 容器 cgroup 内存调优案例 |
| k8s-capacity-planning-qos-cost-optimization | K8s 容量规划方法论与成本优化 — 从流量到资源预算的完整框架,含 QoS 策略、弹性伸缩协同、落 |
| k8s-java-memory-tuning-production-guide | Kubernetes 下 Java 内存调优完整指南 — 内存预算模型、生产参数配置、四层诊断流程、 |
| resource-rbac-scheduling-troubleshooting | K8s 资源配额/OOMKilled 排障 |