netty堆外内存泄漏的典型表现为rss持续增长而堆内存正常,根本原因是bytebuf引用计数未归零导致directbytebuffer无法释放;需通过platformdependent.useddirectmemory()监控、resourceleakdetector.paranoid级检测、堆转储分析refcnt及排查常见泄漏代码模式来定位。

Netty 堆外内存未显式释放,最典型的表征是:JVM 堆内存正常(GC 平稳、老年代不涨),但进程 RSS(Resident Set Size)持续攀升,最终耗尽系统物理内存——而 DirectByteBuffer 本身可能早已被 GC 回收,却因 ByteBuf 引用计数未归零,导致底层堆外内存块迟迟无法释放。定位关键不在“谁分配了”,而在“谁还持有引用”。
看 Netty 自带的内存监控指标
Netty 通过 PlatformDependent 维护全局堆外内存使用量,这是最直接、最可信的第一手数据:
- 读取
PlatformDependent.usedDirectMemory()(4.1.90+ 版本推荐)或反射获取DIRECT_MEMORY_COUNTER的当前值 - 对比
-XX:MaxDirectMemorySize配置值(注意:若未显式设置,默认≈堆最大值-Xmx) - 若
usedDirectMemory接近甚至超过上限,且随时间单向增长,基本确认是 Netty ByteBuf 未 release
开启 Netty 资源泄漏检测(ResourceLeakDetector)
这是定位“哪个 handler 漏放”的核心手段,必须开启(开发/测试环境默认已开,生产建议设为 PARANOID 级别):
- JVM 启动参数加:
-Dio.netty.leakDetectionLevel=paranoid - 或代码中提前设置:
ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID) - 一旦发生泄漏,日志会明确打出:
LEAK: ByteBuf.release() was not called before it's garbage-collected,并附上完整创建栈(精确到channelRead0或write所在 handler 行号) - 注意:该检测有性能开销,生产环境启用时建议配合采样(
SIMPLE或ADVANCED)
分析堆转储(Heap Dump)中的 DirectByteBuffer 和 PooledByteBuf
当 RSS 持续上涨但 GC 日志未见异常时,需确认是否真有大量 DirectByteBuffer 实例残留(非泄漏,而是 GC 滞后):
- 用
jmap -dump:format=b,file=heap.hprof <pid></pid>获取堆快照 - 用 Eclipse MAT 或 JProfiler 打开,搜索
java.nio.DirectByteBuffer—— 若数量远超业务连接数(比如几千个连接却有上万个 DirectByteBuffer),说明 Cleaner 未及时触发或 GC 压力大 - 更关键的是搜索
io.netty.buffer.PooledByteBuf(或UnpooledDirectByteBuf)并查看其refCnt字段:若大量对象refCnt > 0,说明还在被 ChannelHandler、Promise、Future 等强引用,根本没走到 GC 阶段
检查常见泄漏场景与代码模式
多数泄漏并非随机发生,而是集中在几类典型写法:
-
Handler 中未调用
ReferenceCountUtil.release(msg):尤其在exceptionCaught、userEventTriggered、channelInactive等非常规路径中遗漏 -
将
ByteBuf存入异步队列或缓存(如ConcurrentLinkedQueue、Caffeine)后忘记 release:Buf 进队列即脱离 Netty 生命周期管理 -
使用
CompositeByteBuf但未对 component 调用release():Composite 本身 release 不等于内部子 buf 释放 - 自定义
Encoder/Decoder中,在encode失败或decode异常时提前 return,跳过了buf.release() - 将
ByteBuf传给非 Netty 组件(如 Jackson、Protobuf)做序列化后,误以为已“消费”,实际未 release










