DirectByteBuffer引发OutOfMemoryError: Direct buffer memory的根本原因是堆外内存被长期占用且未及时释放,因Cleaner触发延迟导致GC无法立即回收。

DirectByteBuffer 为什么会引发 OutOfMemoryError: Direct buffer memory
根本原因不是“内存不够”,而是堆外内存被大量 DirectByteBuffer 实例长期占用,且未及时释放。每个 DirectByteBuffer 对象本身很小(在堆里),但它背后绑着一块操作系统分配的堆外内存(可能几MB甚至更大)。只要这个对象没被 GC 回收,那块堆外内存就一直占着——而 GC 回收它,又依赖于 Cleaner 的触发时机,存在明显延迟。
常见错误现象:
- 服务运行几小时后突然报
OutOfMemoryError: Direct buffer memory,但堆内存(java.lang.OutOfMemoryError: Java heap space)完全正常 -
jstat -gc显示 GC 频率低、老年代稳定,但系统内存持续上涨,top看进程 RES 占用飙升 - 用
jcmd <pid> VM.native_memory summary</pid>查到Internal或Other区域持续增长(JDK 11+ 更准)
怎么安全地手动释放 DirectByteBuffer 的堆外内存
紧急兜底可用,但别当常规手段——它依赖 JDK 内部 API(sun.misc.Cleaner),在 JDK 9+ 模块化后默认被封禁,需加启动参数才能访问;JDK 17+ 更进一步限制反射调用。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 只在明确知道缓冲区已不用、且无法等待 GC 时调用,例如:自定义协议解析后立即清理临时大缓冲区
- 必须先判断
buffer.isDirect(),否则对堆内缓冲区调用会抛NoSuchFieldException - 加
try-catch吞掉反射异常,避免因 JDK 版本差异导致流程中断 - 示例代码中不要硬写
sun.misc.Cleaner,应通过MethodHandles.privateLookupIn(JDK 15+)或兼容 fallback 方式封装
典型不安全写法:buffer.cleaner().clean() —— 这行在 JDK 14+ 直接编译失败,因为 cleaner() 已被移除。
ByteBuffer.allocateDirect() 的替代方案与池化实践
频繁创建/销毁直接缓冲区是绝大多数堆外 OOM 的源头。与其事后释放,不如从源头控制分配行为。
推荐做法:
- 用
Netty的PooledByteBufAllocator替代裸allocateDirect(),它内部基于内存页 + slab 分配,复用率高、延迟低 - 若不用 Netty,可考虑
Apache Commons Pool自建ByteBuffer池,但注意池大小要合理(过大浪费内存,过小仍频繁分配) - 设置合理的缓冲区容量:避免“宁大勿小”思维,比如 HTTP body 解析用 8KB 足够,没必要默认 1MB
- 配合 JVM 参数
-XX:MaxDirectMemorySize=512m显式限制上限,让问题提前暴露,而不是等 OS 杀进程
监控和定位堆外内存泄漏的真实路径
别只盯着堆 dump(hprof),它几乎看不到 DirectByteBuffer 占用的堆外内存——那些内存压根不在堆里。
有效手段:
- 启用 NIO 缓冲池监控:JDK 自带
java.nio:type=BufferPool,name=direct这个 MBean,可通过 JConsole 或 Prometheus + JMX Exporter 抓取TotalCapacity和Count指标,趋势异常上涨就是信号 - 用
jcmd <pid> VM.native_memory detail</pid>(需开启-XX:NativeMemoryTracking=detail)查看实时堆外各区域用量,重点关注Internal(DirectByteBuffer 主要归属) - 线上慎用
jmap -dump,它不会包含堆外内存信息;真正需要 dump 堆外状态时,可用gcore+gdb(高级场景,运维配合) - 检查是否漏关
FileChannel、SocketChannel或未release()NettyByteBuf——这些资源背后往往藏着未释放的DirectByteBuffer
最容易被忽略的一点:很多框架(如 Spring WebFlux、gRPC)底层自动使用直接缓冲区,你没写 allocateDirect,但流量一上来,它就在悄悄涨。得看文档确认其缓冲策略,必要时覆盖默认 allocator。








