Netty直接内存溢出主因是ByteBuf未显式release导致native内存泄漏;根本在于引用计数机制,需手动释放主动创建、增引用获得及非channelRead0入参的ByteBuf,并用try-finally兜底异常路径。

Netty 中的直接内存溢出,多数不是 JVM 堆内存撑爆,而是操作系统级的 native 内存被 ByteBuf 持续占用却未归还,最终触发 OutOfDirectMemoryError。根本原因在于 Netty 的 引用计数机制 和 对象池设计 —— 它不依赖 GC 回收堆外内存,而靠开发者显式调用 release() 把缓冲区交还池中。一旦漏掉这一步,ByteBuf 就“消失”在 GC 视野里,但池外的 direct memory 仍被锁定,越积越多。
哪些 ByteBuf 必须手动 release
不是所有 ByteBuf 都要你操心释放,关键看谁“最后持有”它:
- 继承
SimpleChannelInboundHandler并重写channelRead0:Netty 自动释放入参msg,无需手动调用release() - 使用
ctx.alloc().buffer()或.directBuffer()主动创建的 ByteBuf:必须自己负责释放,无论是否发送出去 - 从入参
msg中调用readBytes()、copy()、retainedDuplicate()等增引用方法得到的新 ByteBuf:引用计数 +1,原 msg 释放了,新 buffer 还得你来 release - 在
channelRead(非 channelRead0)或exceptionCaught中拿到的msg:Netty 不自动释放,必须显式((ReferenceCounted) msg).release()
释放时机与异常兜底很关键
释放不能只写在正常流程末尾,更要覆盖异常路径。否则一次异常就漏一个 buffer:
- 用
try-finally包裹处理逻辑,finally里执行buffer.release() - 如果 buffer 要传递给下游异步任务(如线程池、CompletableFuture),需先调用
retain()增加引用,等下游用完再各自release() - 发送时用
ctx.writeAndFlush(buffer):Netty 会在写入完成后自动 release,此时你不该提前 release,否则会抛IllegalReferenceCountException - 若发送失败(如 channel inactive),writeAndFlush 不会释放,需在
exceptionCaught或channelInactive中检查并释放待发 buffer
如何快速定位泄漏点
光靠日志报错很难定位到哪行代码没 release,得靠 Netty 自带的泄露检测机制:
- 启动时加 JVM 参数:
-Dio.netty.leakDetection.level=advanced(可选paranoid,但性能损耗大) - 看到
LEAK: ByteBuf.release() was not called before it's garbage-collected日志时,注意后面跟着的Recent access records,它会列出 ByteBuf 创建和最近几次访问的完整调用栈 - 重点关注
Created at行,通常指向ctx.alloc().xxxBuffer()或解码器中in.readBytes()类调用 - 配合
jcmd查看 direct memory 实时占用,验证是否随请求增长VM.native_memory summary
预防比修复更有效
靠人盯代码容易疏漏,建议从工程层面加固:
- 统一封装工具类,比如
ByteBufs.safeWrite(ctx, data),内部自动处理 retain/release 边界 - 在自定义解码器(
ByteToMessageDecoder)中,对out.add(byteBuf)前确保该 buffer 已完成读取且不再需要原始引用 - 禁用
PooledByteBufAllocator改用UnpooledByteBufAllocator仅用于调试 —— 它不复用内存,每次 new + GC,虽慢但不会泄漏,能快速验证是否真为释放问题 - 升级 Netty 版本至 4.1.100+ 或 4.2.x,避开已知的
Recycler缓存无限增长、ChannelOutboundBuffer统计偏差等底层 bug










