ByteBuf是 Netty 性能设计的核心:读写索引减少模式切换,池化分配器减少内存申请,直接内存贴近网络 I/O,引用计数负责生命周期。但这四个能力叠加后,ByteBuf 不再是普通 byte 数组;它被当成有所有权、有生命周期、可共享视图的资源对象来管理。
很多 Netty 内存问题不是“不会用 ByteBuf”,而是“把 ByteBuf 当成普通对象”。普通对象交给 GC,方法返回后忘记也没关系;ByteBuf 尤其是池化直接内存,必须在引用计数归零后归还池。忘记 release 会泄漏,提前 release 会非法访问,跨线程异步使用不 retain 会偶发崩。
Netty 设计 ByteBuf 是为了替代 JDK ByteBuffer 的几个痛点:
• position/limit模式切换复杂。• 堆内存和直接内存使用不统一。 • 切片和组合不够贴合网络协议。 • 高频分配释放成本高。 • 网络框架需要显式管理非堆内存。
源码基线:
• Netty netty-4.1.135.Final• Commit: f05f765d81460799c53123a207f665bf3b465171• 文件: ByteBuf.java、AbstractByteBuf.java、PooledByteBufAllocator.java• 链接:https://github.com/netty/netty/tree/netty-4.1.135.Final/buffer/src/main/java/io/netty/buffer

这张架构图可以从线程缓存开始读:小块内存优先命中 PoolThreadCache,未命中才进入 Arena、Chunk、Page/Subpage 的层级分配。它解释了为什么 Netty 释放 ByteBuf 后进程内存不一定立刻下降——很多内存被留在池里等待复用。判断泄漏时不能只看 RSS,要结合活动分配、写缓冲积压和引用计数。
1. ByteBuf 用两个索引消除 flip 思维
JDK ByteBuffer 读写共用 position,需要写完 flip() 再读,读完 compact() 或 clear()。对网络协议来说,这非常容易错。
ByteBuf 采用:
0 <= readerIndex <= writerIndex <= capacity区域含义:
0 -------- readerIndex -------- writerIndex -------- capacity| 已读可丢弃 | 可读字节 | 可写空间 |写入推进 writerIndex,读取推进 readerIndex。因此可以一边接收字节,一边解析帧,不需要整体切换模式。
典型读取:
if (buf.readableBytes() < 4) { return;}int len = buf.getInt(buf.readerIndex());if (buf.readableBytes() < 4 + len) { return;}buf.skipBytes(4);ByteBuf frame = buf.readRetainedSlice(len);注意 getInt 不推进 readerIndex,适合窥探长度;readInt 会推进索引。Decoder 中先用 get 方法判断完整性,再用 read 方法消费,是避免半包误推进的关键。
2. 容量扩展不是无限制复制
AbstractByteBuf.ensureWritable() 会在写入前确保有足够空间。如果容量不够,计算新容量并调用具体实现扩容。不同 ByteBuf 类型扩容成本不同:
• Unpooled heap:可能新建 byte[] 并复制。 • Unpooled direct:可能重新申请直接内存并复制。 • Pooled ByteBuf:可能从池中重新分配或调整。 • CompositeByteBuf:可能通过组件组合减少复制。
协议设计应尽量避免频繁小步扩容。能预估长度时预分配;大对象传输尽量分块或使用 FileRegion;Decoder 中不要把无上限数据一直累积在一个 Buffer。
3. Direct Memory 的收益和风险
网络 I/O 最终需要和内核交互。直接内存可以减少某些堆内到直接内存的中间复制,尤其适合 SocketChannel 读写。Netty 服务端通常默认偏向使用 direct buffer。
风险在于直接内存不由普通 Java 堆 GC 直接约束。对象引用被 GC 后 Cleaner 可能释放直接内存,但池化和高性能框架不能依赖不确定的 GC 时机。因此 Netty 用引用计数显式管理。
如果线上看到堆内存正常、进程 RSS 或 direct memory 持续增长,排查方向应包括:
• ByteBuf 泄漏。 • Pooled allocator 缓存增长。 • 大连接写缓冲积压。 • CompositeByteBuf 组件未释放。 • 异常分支吞掉消息。
不要只通过调大 MaxDirectMemorySize 掩盖问题。
4. 池化分配器按 Arena 与线程缓存降低竞争
PooledByteBufAllocator 不是一个全局大锁池。它把内存分配拆成多个层次:
PooledByteBufAllocator -> HeapArena / DirectArena -> PoolChunkList -> PoolChunk -> Page / Subpage -> PoolThreadCache线程优先从自己的 PoolThreadCache 获取小块内存,未命中再进入 Arena。Arena 负责从 Chunk 中切分 Page 或 Subpage。这样既减少系统调用,也降低多线程争用。
为什么要区分 tiny/small/normal/huge 等尺寸类别?因为网络消息大小分布通常不均匀。大量小 Buffer 适合缓存和 Subpage,大块 Buffer 需要从 Chunk 切分,超大对象可能不适合进入普通池结构。
池化不是免费午餐。它会让已释放内存留在池里复用,监控上看起来进程内存不一定马上下降。因此判断泄漏不能只看“释放后 RSS 是否立即降低”,要结合 Netty allocator 指标、active allocations 和 leak detector。
5. 引用计数是所有权协议
ByteBuf 实现 ReferenceCounted:
int refCnt();ByteBuf retain();boolean release();基本规则:
allocate -> refCnt = 1retain -> refCnt + 1release -> refCnt - 1refCnt=0 -> 内存释放或归还池谁拿到所有权,谁负责释放。Pipeline 中常见所有权转移:
• 入站 read 成功后,Unsafe 把 ByteBuf 交给 Pipeline。 • Handler 继续传播,就不释放。 • Handler 消费终止,就释放。 • Decoder 产出新消息,要处理旧 Buffer 与新消息的引用关系。 • 出站 write 后,Netty 在发送完成或失败时释放。
引用计数最容易在异步场景出错:
public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf buf = (ByteBuf) msg; executor.submit(() -> parse(buf)); // 错:可能已被释放 ctx.fireChannelRead(msg);}如果异步任务需要使用:
ByteBuf retained = buf.retainedDuplicate();executor.submit(() -> { try { parse(retained); } finally { retained.release(); }});ctx.fireChannelRead(buf);6. slice、duplicate 与 copy 的差别决定安全性
常见方法:
slice() | |||
duplicate() | |||
retainedSlice() | |||
retainedDuplicate() | |||
copy() |
slice() 很快,因为不复制,只创建视图。但它共享底层内存和生命周期。若原 Buffer release 到 0,slice 也不可访问。需要把切片交给下游或异步保存时,优先用 readRetainedSlice() 或 retainedSlice()。
协议解码中,readSlice(len) 和 readRetainedSlice(len) 差别非常重要。前者适合在当前调用栈内立即消费,后者适合把帧作为独立消息传给后续 Handler。
7. CompositeByteBuf 支持逻辑连续,避免物理复制
TCP 半包和粘包处理中,多个 ByteBuf 可以组合成一个逻辑连续 Buffer。CompositeByteBuf 维护组件列表,让读取跨组件进行,而不一定把所有字节复制到新数组。
这对大消息和聚合协议有价值,但组件数量过多也会增加索引查找和释放复杂度。Netty 的 ByteToMessageDecoder 提供 MERGE_CUMULATOR 和 COMPOSITE_CUMULATOR,前者合并复制,后者组合减少复制。选择哪一个要看消息大小、碎片程度和 CPU/内存权衡。
小消息高频场景,合并复制可能更简单快速;大消息或零拷贝敏感场景,组合更有价值。
8. ResourceLeakDetector 是兜底,不是常态依赖
Netty 的 ResourceLeakDetector 会采样跟踪 ByteBuf 的访问路径。当对象被 GC 发现未 release 时,打印泄漏报告。级别包括 disabled、simple、advanced、paranoid 等。
高级别能提供更多访问记录,但开销也更大。生产环境常用 simple 或按需临时提高。压测和开发环境可以打开 advanced 定位具体分支。
泄漏报告要重点看最近的 acquire/touch 记录,而不是只看最终 GC。可以在关键转交点调用:
buf.touch("after decode requestId=...");为泄漏报告添加业务线索。
9. ByteBuf 与 Pipeline 的所有权边界
结合前几篇,入站所有权变化是:
Allocator 分配 -> Channel read 填充 -> pipeline.fireChannelRead -> Decoder 消费/产出 -> Business 消费或继续传播 -> 最终 release出站所有权变化是:
Business 创建响应 -> Encoder 创建 ByteBuf -> unsafe.write 入队 -> doWrite 写出 -> ChannelOutboundBuffer.remove -> release + promise success所以业务不应在 writeAndFlush(buf) 之后立即 release;也不应把入站 ByteBuf 保存到成员变量后忘记 retain;更不应在多个 Channel 之间复用同一个 ByteBuf 实例。

这张生命周期图是写 Netty Handler 时最需要反复对照的检查表。retain 表示多一个所有者,release 表示当前所有者交还资源,refCnt=0 后再访问就是非法。slice/duplicate 只是视图共享,不会天然复制内存;如果要把切片传给下游或异步线程,必须使用 retainedSlice、retainedDuplicate 或显式 retain。
10. 常见内存故障与判断
第一,直接内存泄漏。表现为运行时间越长 direct memory 越高,Full GC 无明显改善,ResourceLeakDetector 可能报警。重点检查异常分支和自定义 Decoder。
第二,提前释放。表现为 IllegalReferenceCountException: refCnt: 0。重点检查 SimpleChannelInboundHandler 自动释放后仍异步使用,或者 write 后手动 release。
第三,池缓存导致“看似不释放”。释放逻辑正确,但 allocator 缓存保留内存以备复用。结合活动分配数和吞吐变化判断,不要误判为泄漏。
第四,写缓冲积压。ByteBuf 没泄漏,只是大量消息还在 ChannelOutboundBuffer 等待慢客户端。此时 refCnt 正常,pending outbound bytes 高,writability 变 false。
第五,Composite 组件泄漏。聚合时只释放外层或只释放内层都可能出错,应遵循 CompositeByteBuf 对组件所有权的 API 约定。
11. 编写 ByteBuf 代码的原则
1. 谁最后消费,谁 release。 2. 要传给异步线程,先 retain 或 retainedDuplicate。 3. 要切片长期持有,使用 retainedSlice。 4. Decoder 判断完整性时用 get,不要提前推进 readerIndex。 5. 出站交给 Netty 后不要手动释放。 6. 异常分支和 early return 必须清理。 7. 优先使用 Netty 提供的解码基类,少手写累积 Buffer。 8. 压测时打开 leak detector 验证。
一个安全模板:
ByteBuf out = ctx.alloc().buffer();try { encode(msg, out); ctx.write(out); out = null; // 所有权转移给 Netty} finally { if (out != null) { out.release(); }}12. 总结:ByteBuf 的强大来自显式生命周期
ByteBuf 的性能优势来自读写索引、池化、直接内存和零拷贝视图;它的复杂性也来自这些设计。普通 Java 对象只需要关心引用可达性,ByteBuf 还要关心所有权份数。
主线记成:
分配 -> 写入 -> 传播 -> 切片/组合 -> retain/release -> 归还池下一篇进入 ByteToMessageDecoder。它是 ByteBuf 与业务消息之间的关键桥梁:如何保存半包、如何循环解码、如何避免 decode 不前进导致死循环,以及 cumulation 策略如何影响复制和内存占用。
参考资料
1. ByteBuf.java:https://github.com/netty/netty/blob/netty-4.1.135.Final/buffer/src/main/java/io/netty/buffer/ByteBuf.java2. AbstractByteBuf.java:https://github.com/netty/netty/blob/netty-4.1.135.Final/buffer/src/main/java/io/netty/buffer/AbstractByteBuf.java3. PooledByteBufAllocator.java:https://github.com/netty/netty/blob/netty-4.1.135.Final/buffer/src/main/java/io/netty/buffer/PooledByteBufAllocator.java4. Netty Reference counted objects:https://netty.io/wiki/reference-counted-objects.html
夜雨聆风