堆外直接内存溢出由directbytebuffer未及时回收导致,表现为rss持续上涨而堆内存稳定;需通过jstat、jmap、nmt和jstack定位cleaner滞后或引用残留,并配置-xx:maxdirectmemorysize及资源池化释放。

堆外直接内存(Direct Memory)本身不归JVM堆内GC管理,它的释放依赖于DirectByteBuffer对象的可达性——只有当该对象被GC回收时,其持有的堆外内存才会通过Cleaner机制异步释放。但这个过程存在天然滞后:GC可能迟迟不触发、或Cleaner线程积压、或System.gc()被禁用,导致堆外内存“已分配却未释放”,形成断层式增长——即堆内占用平稳,而物理内存持续攀升,最终突破-XX:MaxDirectMemorySize或系统限制,触发OutOfMemoryError: Direct buffer memory。
确认是否为Direct Memory断层溢出
先排除堆内、Metaspace、线程栈等干扰项:
- 用
jstat -gc <pid></pid>观察YGC/FGC频率和老年代使用率,若堆内存稳定(如OldUsed长期 - 检查JVM参数是否存在
-XX:+DisableExplicitGC——它会抑制Cleaner触发所需的显式GC调用,加剧滞后 - 用
jmap -histo:live <pid> | grep DirectByteBuffer</pid>查看堆内DirectByteBuffer实例数,若数量大且长期不降,说明对象未被回收
启用NMT精准定位堆外分配源头
在启动参数中加入-XX:NativeMemoryTracking=detail,运行后执行:
-
jcmd <pid> VM.native_memory baseline</pid>(建立基线) - 等待一段时间或复现压力后,执行
jcmd <pid> VM.native_memory summary.diff scale=MB</pid> - 重点关注
Internal和Other区域增长——DirectByteBuffer分配计入Internal;若这两项飙升而Java Heap无明显变化,基本锁定Direct Memory问题
追踪Cleaner执行瓶颈
DirectByteBuffer的清理由java.lang.ref.Cleaner(JDK9+)或sun.misc.Cleaner(JDK8)负责,其执行依赖后台线程:
- 用
jstack <pid></pid>搜索Cleaner线程状态,常见阻塞点:线程池满、Unsafe.freeMemory被native层卡住、或大量Cleaner待处理导致队列堆积 - 检查是否有自定义
Cleaner注册逻辑,或第三方库(如Netty的PooledByteBufAllocator)未正确调用release(),造成引用残留 - 临时验证:在关键路径末尾手动调用
System.gc()(仅调试),若堆外内存回落明显,说明滞后是主因
代码与配置双路收敛
从根源切断断层链:
- 禁止隐式分配:避免
ByteBuffer.allocateDirect()裸用,统一走资源池(如Netty的ByteBufAllocator)并确保release()成对调用 - 调优参数:
-XX:MaxDirectMemorySize设为明确值(如512m),避免默认取-Xmx导致误判;JDK8可加-XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled提升Cleaner响应 - 监控补位:用
/proc/<pid>/smaps</pid>定期抓取Anonymous和File内存页分布,结合pmap -x <pid></pid>识别64MB等典型DirectBuffer内存块










