Netty 用池化 ByteBuf 减少频繁分配和复制,但性能收益附带一个责任:谁保留缓冲区,谁就必须最终释放。引用计数错误会造成泄漏或提前释放。
Table of contents
Open Table of contents
ByteBuf 比 ByteBuffer 多了什么
ByteBuf 分开维护读索引和写索引,不必依赖 flip() 切换模式;它还能按需扩容、切片,并由分配器选择堆内或直接内存。
池化分配器复用大块内存中的区域,降低高频请求的分配开销。
池化不等于容量无限。长期持有小切片可能让背后的大缓冲区无法归还;写队列没有上限也会占满直接内存。
引用计数就是所有权账本
引用计数大于 0 表示内存仍有效。retain() 增加一个所有者,release() 放弃一个所有权;减到 0 后再访问会抛出
IllegalReferenceCountException。
运行ByteBuf 生命周期样例下载:
mvn -Dtest=NetworkServerLabTest#releasesReferenceCountedBuffers test
样例在 finally 中释放自己分配的缓冲区。入站 Handler 是否需要释放取决于基类和是否继续传递:
SimpleChannelInboundHandler通常在回调后自动释放已匹配消息;ChannelInboundHandlerAdapter不会自动替你释放;- 把消息传给下一个 Handler 时,所有权随传播规则转移;
- 异步保存缓冲区时必须
retain,任务结束后对应release。
每次 retain 都必须能指出对应释放点。用“多 retain 一次保险”只会把崩溃改成泄漏。
切片和复制的区别
slice()、duplicate() 通常共享底层存储,修改可能互相可见,生命周期也相互关联;copy() 创建独立内容,成本更高但所有权清晰。
跨异步边界时,要么明确保留共享缓冲区,要么复制成独立、不可变的业务对象。
泄漏排查顺序
- 区分 Java 堆、直接内存、线程栈、映射文件和本地库内存;
- 观察进程 RSS、直接缓冲区池、Netty 分配器和写队列指标;
- 在测试或灰度提高 Netty 泄漏检测级别,保存分配访问轨迹;
- 检查异常、取消、超时、连接关闭等非正常分支是否释放;
- 用最小复现验证修复,不要只靠扩大
MaxDirectMemorySize。
Netty 官方的引用计数对象说明给出了入站、出站消息的所有权规则。 高检测级别会增加开销,生产启用前要评估采样策略。
下一步
阅读从 Netty 源码理解启动与读写链路,把这些组件放回实际调用主线。