排查netty堆外内存泄漏需先确认分配与未释放主体,核心是监控platformdependent.useddirectmemory()、启用paranoid级resourceleakdetector、检查refcnt及高频泄漏模式如异常路径未释放、slice后跨线程未retain等。

排查 Netty 堆外内存 ByteBuf 泄漏,核心是确认“谁分配了内存”和“谁没释放”,而不是盯着堆内对象。因为 ByteBuf 背后可能是 DirectByteBuffer 或池化堆外内存,不受 GC 直接回收——引用计数不归零,内存就一直占着操作系统物理内存(RSS/RES)。
看 Netty 自带的内存使用指标
最直接、最可信的第一步是查 Netty 内部统计:
- 调用 PlatformDependent.usedDirectMemory() 获取当前已分配但未释放的堆外字节数(Netty 4.1.90+ 推荐)
- 对比 JVM 参数 -XX:MaxDirectMemorySize(未设置时默认 ≈ -Xmx),如果 usedDirectMemory 持续单向增长、逼近上限,基本锁定是 ByteBuf 未 release
- 可通过 JMX 访问 io.netty:type=BufferPool,name=direct 查看 direct buffer 总数、内存用量、chunk 分配情况
开启 ResourceLeakDetector 并设为 PARANOID 级别
这是定位泄漏源头的刚性手段:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- JVM 启动时加参数:-Dio.netty.leakDetection.level=PARANOID(必须,simple 级别几乎无效)
- PARNOID 会为每个 ByteBuf 分配注册追踪器,一旦 GC 前未 release,立即打印 LEAK 日志,附带完整创建栈,精确到 channelRead0() 所在 handler 行号
- 日志示例:LEAK: ByteBuf.release() was not called before it's garbage-collected: ... at MyHandler.channelRead(MyHandler.java:42)
- 生产环境启用需权衡性能(约降 20%~30%),但复现期值得;可配合 -Dio.netty.leakDetection.targetRecords=8 控制日志量
分析堆转储中的 refCnt 和持有链
当 RSS 持续上涨但堆内正常时,堆 dump 仍有价值:
- 用 jmap -dump:format=b,file=heap.hprof
抓取快照 - 用 MAT 或 JProfiler 搜索 io.netty.buffer.PooledByteBuf 或 UnpooledDirectByteBuf
- 重点查看其 refCnt 字段:若大量对象 refCnt > 0,说明仍被 ChannelHandler、Promise、Future、静态集合或线程局部变量强引用,根本没走到 GC 阶段
- 再检查 java.nio.DirectByteBuffer 实例数:远超连接数(如 5k 连接却有 2w+ DirectByteBuffer),说明 Cleaner 回收滞后或引用未断
盯紧高频泄漏代码模式
多数泄漏集中在 handler 的异常路径和跨线程场景:
- channelRead 中 try-finally 缺失或 catch 里没兜住 release:输入 ByteBuf 必须在 finally 释放,哪怕 process() 抛异常
- duplicate()/slice() 后传给异步线程却没 retain():副本共享底层内存和 refCnt,原 buf 一 release,副本立刻失效
- ctx.write() 后提前 return,忘了 release 原 msg:write 不等于消费,只要没 writeAndFlush + handler 链走完,你仍需负责释放
- 误信 SimpleChannelInboundHandler 自动释放:它只在 channelRead0() 正常返回后 release,一旦你存入静态 Map、扔进线程池、或异常中 return,就失效
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










