Skip to content
Charles Shao
Go back

Netty 深挖 · ByteBuf、引用计数与零拷贝内存模型

views

开篇讲清了 Reactor 与 EventLoop——数据在 EventLoop 上流转,但数据本身装在哪儿?答案是 ByteBuf。竞价 Server 每秒要分配、读写、释放几十万个缓冲区,内存模型没吃透,轻则 GC 抖动、重则堆外内存泄漏把整个进程打挂。

ByteBuf 用「双指针 + 池化 + 引用计数」把高频分配堆外内存做到近乎免费,代价是你得自己算清 release 的所有权。

TL;DR

Table of contents

Open Table of contents

1. 为什么不用 java.nio.ByteBuffer

JDK 原生的 ByteBuffer 有几个让人反复踩坑的设计:

Netty 的 ByteBuf 把这些全重做了:双指针免 flip、可动态扩容、可池化、带引用计数、支持零拷贝复合。对一个每秒分配几十万缓冲区的竞价 Server 来说,这不是「更好用」,而是「能不能扛住」的分水岭。

2. 三段式双指针:readerIndex / writerIndex / capacity

ByteBuf 用两个独立指针把一段内存切成三块:

![ByteBuf 三段式双指针布局:[0, readerIndex) 为已读可丢弃区、[readerIndex, writerIndex) 为可读内容区、writerIndex, capacity) 为可写空闲区;read* 只推 readerIndex、write* 只推 writerIndex,读写互不干扰,无需 flip()

两个独立指针把内存切成「已读丢弃 / 可读内容 / 可写空闲」三段;读只推 readerIndex、写只推 writerIndex,彻底告别 flip()。

关键操作:

ByteBuf buf = ctx.alloc().buffer();      // 池化分配(默认 direct)
buf.writeInt(0xCAFEBABE);                // writerIndex += 4
buf.writeBytes(payload);                 // 继续写,只动 writerIndex

int magic = buf.readInt();               // readerIndex += 4,writerIndex 不动
// 读写从不互相干扰,无需 flip()

buf.markReaderIndex();                    // 记录读位置
int type = buf.readByte();
buf.resetReaderIndex();                   // 回退重读(解码半包时常用)

buf.discardReadBytes();                   // 回收 [0, readerIndex),腾出空间

ByteBuf 还能自动扩容:写超过 capacity 时会在 maxCapacity 范围内自动扩展,省掉手动分配大 buffer 再拷贝的麻烦。

3. 堆内 vs 堆外 ByteBuf

ByteBuf 有两种底层内存:

堆内 heap(HEAP堆外 direct(DIRECT
底层byte[],在 JVM 堆堆外内存(Unsafe/DirectByteBuffer
GC受 GC 管理不受 GC 直接回收,靠引用计数/Cleaner
Socket IO内核读写要多一次堆→堆外拷贝直达内核,少一次拷贝
分配/回收
访问数据array() 直接拿数组,快逐字节走 Unsafe,稍慢

结论:IO 密集(socket 读写)用 direct 更划算(省一次拷贝),但 direct 分配贵、不受 GC 管——这两点正是 Netty 要引入池化(摊平分配成本)和引用计数(不靠 GC 也能及时回收)的根本原因。业务处理时若要频繁按数组随机访问,heap buffer 反而更方便。

关于堆外内存与 GC 的取舍,也可参考本地缓存深挖里堆内/堆外的讨论,以及 JVM 垃圾回收——堆外内存的坑在于它绕开了你熟悉的 GC 心智模型

4. 池化:PooledByteBufAllocator 与 jemalloc 思想

如果每次 IO 都 allocateDirect 一块新堆外内存、用完再回收,高频下开销惊人。Netty 的解法是池化:把内存块预先切好放进池子,分配即取、释放即还。

PooledByteBufAllocator 借鉴 jemalloc 的分层内存池:PoolThreadCache 线程本地缓存命中即无锁复用,未命中走 PoolArena(默认约 2×核数 个以分散锁竞争),再到 PoolChunk(默认 16MB,按伙伴算法切分)→ Page(8KB) → SubPage;分配按 small/normal/huge 分级

借鉴 jemalloc 的分层:Arena 削锁、Chunk/Page/SubPage 分级切分、PoolThreadCache 走无锁快路径,把「高频分配堆外内存」摊到近乎免费。

分层结构:

按请求大小分级:

Netty 4.1 起 PooledByteBufAllocator 是默认分配器。可通过 -Dio.netty.allocator.type=pooled|unpooled 切换,或在 ServerBootstrap 上用 .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) 显式指定。

5. 引用计数:retain / release 与所有权

因为池化 + 堆外内存不受 GC 直接回收,Netty 用引用计数决定何时把内存归还池子:

引用计数与所有权。横向:分配 refCnt=1 → retain/slice → 业务使用 → release 到 0 归还池子;下方三条约定:SimpleChannelInboundHandler 自动释放入站、write 后 Netty 释放出站、retainedSlice/copy 出去的要自己配对 release。底部说明漏 release 导致缓慢 Direct 泄漏。

ByteBuf buf = ctx.alloc().buffer();   // refCnt = 1
buf.retain();                          // refCnt = 2
buf.release();                         // refCnt = 1
buf.release();                         // refCnt = 0 → 内存归还池子
// buf.readByte();                     // 抛 IllegalReferenceCountException

所有权规则(谁最后用完谁 release):

6. 零拷贝:CompositeByteBuf、slice 与 FileRegion

「零拷贝」在 Netty 里主要指避免用户态内存之间的多余拷贝

零拷贝对比:传统方式把 header 和 body 各 memcpy 进一块新分配的 ByteBuf(多一次分配 + 两次拷贝);CompositeByteBuf 只持有 header 和 body 两段的引用,逻辑上是一个连续 ByteBuf,物理上零拷贝,读跨组件时按偏移路由到对应底层 ByteBuf

传统合并要新分配 + 两次 memcpy;CompositeByteBuf 只多一层索引、底层字节原地不动,高频大包时省下的就是纯 CPU 与延迟。

三种手段:

⚠️ 共享底层意味着引用计数也共享slice() 出来的和原 buffer 指向同一块内存,release 责任要算清(见 §7),否则要么提前释放导致 use-after-free,要么漏释放导致泄漏。

7. 实践红线:谁最后 release

内存泄漏几乎都源于 release 责任没算清。几条红线:

  1. 入站优先用 SimpleChannelInboundHandler:自动释放,避免手滑漏 release。若必须用原始 channelReadfinally { ReferenceCountUtil.release(msg); } 兜底。
  2. slice() / retain() 后所有权归你retainedSlice() 会顺带 +1,责任更清晰。
  3. 异常路径也要 release:解码抛异常时别让已分配的 buffer 悬空。
  4. 别 release 后再用:包括别把 release 后的 buffer 传给异步任务。
  5. 开泄漏检测跑压测:上线前用 -Dio.netty.leakDetection.level=paranoid 跑一轮,ResourceLeakDetector 会在 GC 到未 release 的 buffer 时打出分配栈,直接定位到漏 release 的代码。

ResourceLeakDetector 四档:DISABLED(关)、SIMPLE(默认,1% 采样)、ADVANCED(1% 采样 + 记录访问轨迹)、PARANOID(每个 buffer 都查,压测/排查用,生产别开)。

延伸阅读


views
Share this post on:

Previous Post
Netty 深挖 · Pipeline、编解码与粘包拆包
Next Post
Netty 深挖(开篇)· Reactor 模型与 EventLoop 线程模型