Java本地内存溢出在Netty中90%以上源于ByteBuf未显式release导致DirectByteBuffer底层内存块滞留,需从RSS/pmap、Netty引用计数、调用栈火焰图三路并进定位。

Java 本地内存溢出(即堆外内存持续上涨、RSS 暴涨、被 OOM Killer 杀死)在 Netty 场景下,90% 以上都源于 ByteBuf 未显式 release 导致的 DirectByteBuffer 底层内存块长期滞留。它不走 JVM GC 流程,传统 heap dump 看不到泄漏点,必须绕过“堆内视角”,从操作系统层、Netty 引用计数层、内存分配调用栈三路并进。
看 RSS 和 pmap —— 确认是不是堆外问题
别急着看日志或 dump,先看进程真实驻留内存:
- top 或 ps aux 查看 RES(RSS),若远超 -Xmx(比如堆设 512MB,RES 却到 2.3GB),基本排除堆内问题
-
pmap -x <pid> 找大量
[anon]块:尤其是成片的 64MB、128MB、256MB 的 rw--- 匿名映射——这是 Netty PooledByteBufAllocator 分配池块的典型特征 - 加总这些 anon 块 RSS,若占 RES 80% 以上,可锁定为 Netty 堆外内存分配失控
查 Netty 内存水位和泄漏日志 —— 定位“谁没放手”
Netty 自带两把快刀,不用白不用:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- 实时读取 PlatformDependent.usedDirectMemory()(4.1.90+ 推荐),对比 -XX:MaxDirectMemorySize;若值持续单向增长且逼近上限,说明有 ByteBuf 长期 refCnt > 0
- JVM 启动时加参数:-Dio.netty.leakDetection.level=PARANOID,并确保日志能捕获 LEAK 行;每条日志形如:
LEAK: ByteBuf.release() was not called before it's garbage-collected. Created at: io.netty.buffer.AbstractByteBufAllocator.ioBuffer() → com.xxx.handler.MyHandler.channelRead()
→ 直接定位到具体 handler 和代码行 - 若日志量太大影响性能,生产环境可用 ADVANCED + 降低 targetRecords,仍能保留关键泄漏路径
抓调用栈火焰图 —— 追踪 malloc 调用源头
当泄漏点模糊(比如日志只显示通用 Netty 方法),需深入 native 层:
立即学习“Java免费学习笔记(深入)”;
- 用 gperftools(tcmalloc)或 jemalloc 的 profiler 功能,LD_PRELOAD 注入后采集 malloc/free 调用栈
- 重点过滤含
io.netty.buffer、DirectByteBuffer、PooledByteBuf的栈帧;火焰图中“宽底座+高塔”的分支,往往对应高频分配但低释放的业务 handler - 常见泄漏模式包括:WebSocket 消息处理中多次调用
msg.content().retain()却漏掉release();异常分支未包裹 finally;Flux/Mono 中 flatMap 并发处理 ByteBuf 后未统一释放
验 refCnt 和池状态 —— 确认是否真“卡住”
堆转储不是无用,只是要会看:
- jmap -dump:format=b,file=heap.hprof <pid> 后,用 MAT 或 JProfiler 搜索 io.netty.buffer.PooledByteBuf
- 按
refCnt字段排序,若大量对象 refCnt == 2 或 4(非 0 或 1),说明被多个地方引用却无人归还 - 同时搜索 PoolChunk 实例数:若远高于活跃连接数(如 1000 连接却有 8000+ PoolChunk),说明内存池碎片化严重或 chunk 长期未回收,常因 handler 持有 buffer 引用导致整个 chunk 被锁住

















