确认direct memory泄漏需观察三个信号:日志含“direct buffer memory”且堆使用率正常;top中res远超-xmx与metaspace之和;pmap显示gb级匿名内存持续增长;必须启用nmt(-xx:nativememorytracking=summary)并用jcmd对比内存差值,重点关注direct buffer、internal、thread增长;netty场景需设-dio.netty.leakdetection.level=paranoid定位未释放bytebuf。

直接内存(Direct Memory)是JVM堆外内存中最常见、最易出问题的部分。它不归GC管理,但受 -XX:MaxDirectMemorySize 限制;泄漏时不会触发堆OOM,却会悄无声息吃掉物理内存,直到进程被系统OOM Killer干掉。
怎么确认是Direct Memory泄漏?
别只看堆内存——这是最大误区。重点观察三个信号:
-
错误日志出现
OutOfMemoryError: Direct buffer memory,而jstat -gc显示堆使用率正常(比如才50%) -
Linux下
top或htop中Java进程的 RES 持续上涨,且远超-Xmx + -XX:MaxMetaspaceSize之和 -
pmap -x <pid> | grep anon | sort -rn -k3 | head -5</pid>显示多个GB级匿名内存段持续增长
必须开启的诊断开关:NMT
NMT(Native Memory Tracking)是唯一能告诉你“谁在用、用了多少”的官方工具,但它必须启动时就启用:
- 加JVM参数:
-XX:NativeMemoryTracking=summary(生产环境推荐,性能损耗小) - 应用稳定后执行:
jcmd <pid> VM.native_memory baseline</pid> - 运行一段时间再执行:
jcmd <pid> VM.native_memory summary.diff scale=MB</pid> - 重点关注
Direct Buffer、Internal、Thread三栏是否异常增长
定位到代码的关键动作
确认是Direct Buffer泄漏后,不能只查业务代码——框架才是重灾区:
-
Netty场景:启动加
-Dio.netty.leakDetection.level=PARANOID,日志会打印未释放ByteBuf的完整调用栈 -
排查分配点:用
pstack <pid></pid>搜索DirectByteBuffer或PooledUnsafeDirectByteBuf,结合NMT的线程ID反向定位Handler逻辑 -
检查资源释放模式:是否在
ChannelHandler中漏掉buf.release();是否用FileChannel.map()后没调unmap();是否手动new了DirectByteBuffer却没注册Cleaner
写代码时的硬性守则
堆外内存不是“申请了就完事”,它像C语言一样需要显式生命周期管理:
- Netty中所有传入
channelRead的ByteBuf,必须在处理完后调用release()或retain()并明确传递责任 - 使用
try-with-resources包装支持AutoCloseable的堆外资源(如某些自定义Buffer包装类) - 避免在finally里盲目调
System.gc()——它不保证立即回收DirectBuffer,反而干扰GC节奏 - 优先用框架提供的池化对象(如
PooledByteBufAllocator),而非反复allocateDirect()











