跳转到内容

堆外内存与 DirectBuffer

用途:解释“堆没满但内存吃紧”“RSS 远高于 Xmx”这类问题

  1. 避免复制:与内核态/本地库交互时,堆内存需先拷贝到本地缓冲;直接缓冲可省一次拷贝(网络 IO、文件映射)。
  2. 不受 GC 停顿影响:生命周期由显式或引用队列管理,适合大容量池化内存。
  3. 突破堆限制:某些场景需要超大连续内存。

代价:不受堆大小约束、GC 无法及时回收、排障不可见(堆 dump 里没有)、分配与释放错误会直接体现为进程内存增长。

形态 管理者 释放时机 风险
ByteBuffer.allocateDirect JVM 的 Bits 计数器 + Cleaner 依赖 GC 触发清理 分配快于清理时会 OOM 或堆积
Netty PooledByteBufAllocator 池化 arena/chunk,引用计数 release() 归零 忘记 release 造成泄漏(可用泄漏检测档)
Unsafe/JNI/MMap 手工 手工 free 最难排查,需要专项审计
  • -XX:MaxDirectMemorySize 限制 allocateDirect 总量(默认约等于 -Xmx,容易与堆叠加超出容器限制)。
  • 观测:BufferPool MBean(direct 的 count/used capacity)、Netty 的池指标、jcmd VM.native_memory summary(需开 NMT)。
  • 容器内务必把“堆 + 元空间 + 线程栈 + 堆外 + 代码缓存”总和与容器限制对齐,留余量。
症状 原因 处理
Direct buffer memory OOM 分配速率高于 Cleaner 回收 复用 buffer、显式限流、必要时手动 clean(谨慎)、降低单次大小
RSS 持续高于 Xmx 且稳定 正常池化占用或 glibc arena NMT 拆解;必要时限制 arena 或换分配器
RSS 持续上涨 本地内存泄漏 审计 Unsafe/JNI/MMap 配对释放;Netty 开泄漏检测定位调用栈
GC 后仍不释放 Cleaner 依赖 GC 触发,老年代回收不频繁 显式释放或限制池大小
  1. 需要堆外就集中封装(一个内存管理组件负责申请/释放/指标),禁止散落 Unsafe。
  2. 有引用计数就要有“归零断言”与泄漏检测(Netty -Dio.netty.leakDetection.level=paranoid 仅用于定位)。
  3. 池大小、单块大小、等待策略都要有指标与告警。
  4. 与容器内存规划一起设计,而不是等 OOMKilled 再查。
误区 修正
不设 MaxDirectMemorySize 堆外与堆叠加会撑爆容器
认为 GC 会及时回收堆外 回收依赖 Cleaner 触发,可能滞后
散落使用 Unsafe 集中封装并配对释放
只看堆指标判断内存健康 必须同时看 RSS、NMT、BufferPool
  • Netty 官方文档:Pooled ByteBufAllocator 与 Leak Awareness
  • OpenJDK java.nio.Bits 与 DirectByteBuffer 实现说明
  • Nitsan Wakart《Off-heap memory in the JVM》相关公开文章