ByteBuf——引用计数、池化与零拷贝
摘要
如果说 EventLoop 是 Netty 的”心脏”,那么 ByteBuf 就是 Netty 的”血液”——网络通信中所有的数据读写都通过 ByteBuf 流转。Netty 没有复用 JDK 的 ByteBuffer,而是彻底重新设计了一套内存缓冲区体系。ByteBuf 相比 ByteBuffer 有三大核心优势:读写双指针(消除了让人头疼的 flip() 调用)、引用计数(堆外内存的精确生命周期管理,防止内存泄漏)、池化内存(通过 jemalloc 算法复用内存块,消除高并发下频繁分配/释放的 GC 压力)。本文从 ByteBuffer 的设计缺陷出发,深入剖析 ByteBuf 的读写指针机制、五种内存分配方式的选择策略、引用计数的工作原理与内存泄漏的排查方法、CompositeByteBuf 实现零拷贝聚合的原理,以及 PooledByteBufAllocator 的核心分配逻辑。
第 1 章 ByteBuffer 的设计缺陷
1.1 单指针模型的困境
上一篇(NIO 基础)中我们介绍了 ByteBuffer 的三属性模型(capacity/position/limit)和令人困惑的 flip() 调用。让我们深入分析这个设计的根本缺陷。
ByteBuffer 用一个 position 指针同时承担”写入位置”和”读取位置”两个职责——写入时 position 向后移动,flip() 之后 position 回到 0 变成读取起始位置。这种”单指针复用”的设计导致:
缺陷一:读写状态不能并存
在 ByteBuffer 中,要么是写模式,要么是读模式,不能同时进行读写。在网络编程中,有一种常见的操作叫”追加写”——先写入一些数据,然后读取验证,再追加写更多数据。用 ByteBuffer 实现这个操作需要频繁调用 flip()/compact(),代码逻辑混乱:
// ByteBuffer 的追加写(繁琐且容易出错)
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.put("Hello".getBytes()); // 写
buf.flip(); // 切换到读模式
byte[] data = new byte[5];
buf.get(data); // 读(验证)
buf.compact(); // 将未读数据移到头部,切换回写模式
buf.put(", World!".getBytes()); // 再追加写
buf.flip(); // 再切换回读模式
// ... 这只是一个简单例子,实际编解码场景更复杂缺陷二:不支持动态扩容
ByteBuffer 是固定容量的,一旦创建不能扩容。网络数据的长度往往是动态的(HTTP 响应体可以从几百字节到几十 MB),预先分配固定大小的缓冲区要么浪费内存(分配过大),要么在数据超出容量时无法处理(分配过小)。
缺陷三:缺少链式操作和便利方法
ByteBuffer 的 API 设计比较原始,没有 writeInt()/writeUtf8String() 这类便利方法,也不支持链式调用,使用体验差。
1.2 ByteBuf 的解决方案
Netty 的 ByteBuf 用两个独立指针彻底解决了这些问题:
┌─────────────────────────────────────────────────────────┐
│ ByteBuf │
│ │
│ 0 readerIndex writerIndex capacity │
│ ├──────────┼──────────────────┼───────────────┤ │
│ │ 已读丢弃区│ 可读区域 │ 可写区域 │ │
│ │(discarded)│ (readable bytes) │(writable bytes)│ │
│ └──────────┴──────────────────┴───────────────┘ │
└─────────────────────────────────────────────────────────┘
readerIndex:下一个要读取的位置,readXxx()后自动前进;writerIndex:下一个要写入的位置,writeXxx()后自动前进;- 两个指针独立,读写互不干扰,不再需要
flip(); capacity可以动态扩展(通过ensureWritable(n)触发)。
// ByteBuf 的读写(直观自然,无需 flip())
ByteBuf buf = Unpooled.buffer(1024);
// 写入(writerIndex 自动前进)
buf.writeBytes("Hello".getBytes());
buf.writeInt(42);
buf.writeLong(System.currentTimeMillis());
// 直接读取(readerIndex 自动前进),无需 flip()
byte[] strBytes = new byte[5];
buf.readBytes(strBytes);
int num = buf.readInt();
long timestamp = buf.readLong();
System.out.println(new String(strBytes)); // "Hello"
System.out.println(num); // 42第 2 章 ByteBuf 的五种分类
2.1 按内存位置分类:堆内 vs 堆外
堆内缓冲区(HeapByteBuf):
内存分配在 JVM 堆上,受 GC 管理。创建快,有 byte[] 作为底层存储,便于直接操作。
缺点:做 I/O 时需要额外的一次内存拷贝——JVM 在执行 channel.write(heapByteBuf) 时,必须先将堆内数据拷贝到堆外临时缓冲区(因为 GC 可能移动堆对象,导致地址不固定,内核 DMA 需要固定地址),再进行 I/O。
// 堆内 ByteBuf
ByteBuf heapBuf = Unpooled.buffer(1024); // 或 ctx.alloc().heapBuffer()
System.out.println(heapBuf.hasArray()); // true:有底层 byte[]
byte[] array = heapBuf.array(); // 直接访问底层数组堆外缓冲区(DirectByteBuf):
内存分配在 JVM 堆外(操作系统内存),不受 GC 管理。I/O 操作时不需要额外拷贝(内核可以直接 DMA 读写这块内存),性能更好。
缺点:分配和释放代价高,不受 GC 管理(需要手动释放,Netty 通过引用计数管理)。
// 堆外 ByteBuf(推荐用于 I/O 操作)
ByteBuf directBuf = Unpooled.directBuffer(1024); // 或 ctx.alloc().directBuffer()
System.out.println(directBuf.hasArray()); // false:没有底层 byte[]
System.out.println(directBuf.isDirect()); // trueNetty 默认使用堆外内存
PooledByteBufAllocator.DEFAULT默认分配堆外直接内存(preferDirect = true),因为 Netty 的主要场景是网络 I/O,堆外内存减少了一次数据拷贝,性能更好。如果你看到 Handler 中创建了堆内 ByteBuf,在写出时 Netty 会自动检测并在写入网卡之前将其转换为堆外内存。这个转换是隐式的,但会有额外的拷贝开销。
2.2 按是否池化分类:池化 vs 非池化
非池化(Unpooled):
每次分配创建新的内存,用完释放。适合临时性、一次性使用的场景。
// 非池化分配(每次 new 一个新的)
ByteBuf buf1 = Unpooled.buffer(1024); // 非池化堆内
ByteBuf buf2 = Unpooled.directBuffer(1024); // 非池化堆外
ByteBuf buf3 = Unpooled.wrappedBuffer(existingByteArray); // 包装已有数组池化(Pooled):
从内存池中取出预先分配好的内存块,用完归还(不真正释放,供下次复用)。适合高并发、频繁分配/释放的场景(如网络服务器的请求/响应处理)。
// 池化分配(从内存池取出)
ByteBuf buf = PooledByteBufAllocator.DEFAULT.buffer(1024);
// 等价于
ByteBuf buf = ctx.alloc().buffer(1024); // 在 Handler 中使用,ctx.alloc() 返回配置的 allocator池化内存的背后是 jemalloc 算法,第七篇将专门讲解。这里只需要记住:高并发场景下,池化内存对性能的提升是数量级的——JVM GC 无需频繁回收大量短生命周期的堆外内存,内存分配/释放的开销接近 O(1)。
2.3 四种组合
实际使用中,ByteBuf 的四种组合各有适用场景:
| 类型 | 创建方式 | 适用场景 |
|---|---|---|
| 池化堆外(默认推荐) | ctx.alloc().directBuffer() | 网络 I/O(减少拷贝 + 池化复用) |
| 池化堆内 | ctx.alloc().heapBuffer() | 业务逻辑处理中频繁创建的中间缓冲区 |
| 非池化堆外 | Unpooled.directBuffer() | 大块临时缓冲区(如文件传输) |
| 非池化堆内 | Unpooled.buffer() | 单元测试、简单场景 |
第 3 章 引用计数:堆外内存的生命周期管理
3.1 为什么堆外内存需要引用计数
JVM GC 只管理堆内内存。堆外内存(DirectByteBuffer)不在 GC 的管辖范围内,如果不主动释放,就会造成堆外内存泄漏。
JDK 的 DirectByteBuffer 依赖 PhantomReference + Cleaner 机制:当 DirectByteBuffer 对象被 GC 回收时,Cleaner 会调用 free() 释放堆外内存。这个机制的问题是:
- 延迟释放:堆外内存的释放依赖 GC 触发,而 GC 时机不可预测,可能积累大量未释放的堆外内存(即使 JVM 堆内存充足,也会因堆外内存耗尽报
OutOfMemoryError: Direct buffer memory); - 无法池化:Cleaner 是单向的(只能释放,不能”归还”到池中),不支持内存池化复用。
Netty 引入**引用计数(Reference Counting)**来精确管理 ByteBuf 的生命周期:
- 每个
ByteBuf都有一个refCnt(引用计数),初始值为 1; retain():增加引用计数(表示又有一个地方持有这个ByteBuf);release():减少引用计数(表示这个持有者不再使用);- 当引用计数降为 0 时:立即释放内存(或归还到内存池)。
// 引用计数的基本操作
ByteBuf buf = ctx.alloc().directBuffer(1024);
System.out.println(buf.refCnt()); // 1(刚创建)
buf.retain(); // 引用计数 → 2(另一个地方持有)
System.out.println(buf.refCnt()); // 2
buf.release(); // 引用计数 → 1
System.out.println(buf.refCnt()); // 1
buf.release(); // 引用计数 → 0,内存被释放(或归还池中)
// 此后不能再使用 buf!访问已释放的 ByteBuf 会抛出 IllegalReferenceCountException3.2 谁负责释放 ByteBuf
引用计数模型要求”谁持有,谁负责最终释放”。在 Netty 的使用规范中:
规则一:入站消息(channelRead 中的 msg)
Netty 读取网络数据后创建 ByteBuf,传入 ChannelPipeline。这个 ByteBuf 的初始 refCnt = 1,责任链中的 Handler 需要确保最终被释放:
- 如果 Handler 消费了消息(不再往下传),必须调用
ReferenceCountUtil.release(msg)释放; - 如果 Handler 转发了消息(调用
ctx.fireChannelRead(msg)),则由下游 Handler 负责; TailContext(Pipeline 末端)会自动释放未被消费的ByteBuf(作为最后防线)。
SimpleChannelInboundHandler<T> 自动处理了这个问题:
public abstract class SimpleChannelInboundHandler<I> extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
boolean release = true;
try {
if (acceptInboundMessage(msg)) {
// 调用子类的业务处理方法
channelRead0(ctx, (I) msg);
} else {
release = false;
ctx.fireChannelRead(msg); // 类型不匹配,往下传
}
} finally {
if (autoRelease && release) {
// 自动释放(无论 channelRead0 是否抛异常)
ReferenceCountUtil.release(msg);
}
}
}
// 子类实现(无需关心 ByteBuf 释放)
protected abstract void channelRead0(ChannelHandlerContext ctx, I msg) throws Exception;
}规则二:出站消息(write 操作中的 msg)
channel.writeAndFlush(msg) 后,Netty 在数据实际写入网络(或写失败)后会自动释放 msg(调用 ReferenceCountUtil.release())。调用者不应该在 writeAndFlush() 之后再次释放,否则引用计数会变为 -1,抛出异常。
规则三:自己创建的 ByteBuf,自己负责释放
如果在 Handler 中创建了 ByteBuf 但没有通过 write() 传出(比如构建了响应但发送失败),必须手动释放:
ByteBuf response = ctx.alloc().buffer();
try {
buildResponse(response);
ctx.writeAndFlush(response);
response = null; // 置 null,避免在 finally 中双重释放
} finally {
if (response != null) {
// 如果 writeAndFlush 之前出现异常,需要手动释放
response.release();
}
}3.3 内存泄漏检测
Netty 提供了内置的内存泄漏检测机制,通过 ResourceLeakDetector 实现:
// 四种泄漏检测级别
// DISABLED:关闭检测(生产环境最高性能,但无法发现泄漏)
// SIMPLE(默认):随机抽样约 1% 的 ByteBuf 进行跟踪,极低开销
// ADVANCED:抽样跟踪,且记录访问位置的完整堆栈(开销中等)
// PARANOID:跟踪所有 ByteBuf,记录完整堆栈(性能影响大,仅用于调试)
// 启动时通过 JVM 参数设置
// -Dio.netty.leakDetectionLevel=ADVANCED
ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID);检测原理:ResourceLeakDetector 使用 PhantomReference(幽灵引用)监控 ByteBuf 对象。当 ByteBuf 对象被 GC 回收时(说明 Java 层的引用都消失了),ResourceLeakDetector 检查引用计数是否已经归零。如果没有(内存未被显式释放),说明发生了内存泄漏,打印警告日志:
LEAK: ByteBuf.release() was not called before it's garbage-collected.
Recent access records:
#1: io.netty.handler.codec.http.HttpObjectDecoder.decode(HttpObjectDecoder.java:284)
#2: io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:275)
...
生产环境建议
建议在测试环境使用
PARANOID级别排查所有内存泄漏,在预发/生产环境使用SIMPLE级别(几乎零开销,但能发现 1% 的泄漏样本)。发现泄漏后,临时切换到ADVANCED级别定位具体的泄漏位置。
第 4 章 ByteBuf 的核心 API
4.1 读写 API
ByteBuf 提供两种读写方式:
相对读写(随 readerIndex/writerIndex 移动):
ByteBuf buf = Unpooled.buffer(64);
// 写入
buf.writeByte(0x01); // 写 1 字节,writerIndex +1
buf.writeShort(0x0203); // 写 2 字节,writerIndex +2
buf.writeInt(0x04050607); // 写 4 字节,writerIndex +4
buf.writeLong(0x0809101112131415L); // 写 8 字节,writerIndex +8
buf.writeBytes("Hello".getBytes()); // 写字节数组,writerIndex +5
buf.writeCharSequence("World", CharsetUtil.UTF_8); // 写字符串(UTF-8 编码)
// 读取(readerIndex 自动前进)
byte b = buf.readByte();
short s = buf.readShort();
int i = buf.readInt();
long l = buf.readLong();
byte[] bytes = new byte[5];
buf.readBytes(bytes); // 读到 byte[]
String str = buf.readCharSequence(5, CharsetUtil.UTF_8).toString();绝对读写(指定下标,不移动 readerIndex/writerIndex):
// 绝对写(指定 index,不改变 writerIndex)
buf.setByte(0, 0xFF);
buf.setInt(4, 12345);
// 绝对读(指定 index,不改变 readerIndex)
byte val = buf.getByte(0);
int num = buf.getInt(4);绝对读写适合在已知偏移量的情况下随机访问数据(如解析固定格式的二进制协议头部)。
4.2 缓冲区查询方法
int readableBytes = buf.readableBytes(); // 可读字节数 = writerIndex - readerIndex
int writableBytes = buf.writableBytes(); // 可写字节数 = capacity - writerIndex
boolean readable = buf.isReadable(); // 是否有数据可读
boolean writable = buf.isWritable(); // 是否有空间可写
int capacity = buf.capacity(); // 当前容量(可动态扩展)
int maxCapacity = buf.maxCapacity(); // 最大容量上限
// 回收已读区域:将 readerIndex 重置为 0,将可读区域移到头部
// 适用于:读了一部分,想从头重新处理
buf.discardReadBytes(); // 释放 [0, readerIndex) 的空间,writerIndex 相应减少4.3 派生缓冲区(Derived Buffers)
Netty 提供了多种从已有 ByteBuf 创建派生缓冲区的方法:
// slice():创建一个共享底层内存的子缓冲区(不拷贝数据)
// 修改 slice 会影响原 ByteBuf,修改原 ByteBuf 也会影响 slice
ByteBuf slice = buf.slice(0, 10); // 取 [0, 10) 的区域
// duplicate():创建一个共享整个底层内存的副本(独立的 readerIndex/writerIndex)
ByteBuf dup = buf.duplicate();
// copy():创建一个完全独立的拷贝(深拷贝,不共享内存)
ByteBuf copy = buf.copy();
// readSlice():从当前 readerIndex 读取 n 字节的 slice(readerIndex 前进 n)
ByteBuf header = buf.readSlice(4);slice 和 duplicate 的引用计数陷阱
slice()/duplicate()创建的派生缓冲区与原缓冲区共享引用计数,但并不自动增加引用计数。如果你将slice传出当前作用域(比如传给另一个 Handler),必须先调用slice.retain()(增加引用计数),否则原缓冲区释放后,slice访问的内存就变成了”悬空指针”。这是
ByteBuf内存泄漏和野指针问题的高发场景之一。
第 5 章 CompositeByteBuf:零拷贝的数据聚合
5.1 聚合多个缓冲区的朴素做法与其代价
网络编程中,经常需要将多个独立的 ByteBuf 组合成一个逻辑上连续的缓冲区。比如:HTTP 响应的头部(Header)和正文(Body)分别存储在不同的 ByteBuf 中,发送时需要合并为一个完整的响应。
朴素做法是分配一个新的 ByteBuf,将两个部分的数据依次拷贝进去:
// 朴素做法:数据拷贝合并(有性能损耗)
ByteBuf header = buildHeader(); // 假设 200 字节
ByteBuf body = buildBody(); // 假设 50000 字节
ByteBuf combined = ctx.alloc().buffer(header.readableBytes() + body.readableBytes());
combined.writeBytes(header); // 数据拷贝!200 字节
combined.writeBytes(body); // 数据拷贝!50000 字节
ctx.writeAndFlush(combined);
header.release();
body.release();
combined.release();这种做法对于大文件传输来说,每次请求都要额外拷贝数万字节,在高并发下会造成可观的 CPU 和内存带宽消耗。
5.2 CompositeByteBuf:逻辑聚合,物理不拷贝
CompositeByteBuf 允许将多个 ByteBuf 组合成一个逻辑上连续的视图,不进行实际的数据拷贝:
// 零拷贝聚合
CompositeByteBuf composite = ctx.alloc().compositeBuffer();
composite.addComponents(true, header, body);
// true 参数表示:自动增加各组件的引用计数,并更新 writerIndex
// 外部使用者看到的是一个连续的缓冲区,但底层是两块独立的内存
System.out.println(composite.readableBytes()); // header.readableBytes() + body.readableBytes()
// 读取时 CompositeByteBuf 负责跨组件的透明读取
byte firstByte = composite.readByte(); // 从 header 读
// ...
ctx.writeAndFlush(composite); // 发送(Netty 的 GatheringByteChannel 可以一次性发送多个缓冲区)
// 注意:不需要手动释放 header 和 body(addComponents 已经 retain,release 由 composite 管理)
composite.release();CompositeByteBuf 内部维护一个组件列表(Component 数组),每个组件记录起始偏移量。读取时,根据当前 readerIndex 计算属于哪个组件,再在该组件的 ByteBuf 上进行实际读取。
这就是 Netty 所谓的”零拷贝”之一——数据在内存中只存在一份,通过逻辑聚合的方式呈现为连续视图,消除了数据拷贝。
5.3 Netty 的其他零拷贝机制
除了 CompositeByteBuf,Netty 还有两种零拷贝技术:
slice 零拷贝:
如前所述,buf.slice(offset, length) 返回的是原缓冲区某段内存的只读视图,不拷贝数据。在解析消息时,可以直接用 slice 引用原始字节数据中的某个字段,而不是将字段数据拷贝到新的 ByteBuf。
FileRegion(sendfile 系统调用):
在 Netty 中,将文件内容发送给客户端时,可以使用 DefaultFileRegion,它封装了 JDK FileChannel.transferTo(),底层触发 Linux 的 sendfile() 系统调用,实现内核态的零拷贝文件传输(前一篇已详细介绍):
// 零拷贝文件发送
FileChannel fileChannel = new FileInputStream("large_file.zip").getChannel();
FileRegion region = new DefaultFileRegion(fileChannel, 0, fileChannel.size());
ctx.writeAndFlush(region); // 底层:sendfile(),不经过用户态第 6 章 ByteBufAllocator:分配器的选择
6.1 三种分配器
Netty 提供了三种 ByteBufAllocator 实现:
PooledByteBufAllocator(默认,推荐):
基于内存池的分配器,采用 jemalloc 算法管理内存块复用。适合高并发、频繁创建/释放 ByteBuf 的场景。这是 Netty 4.x 之后的默认分配器。
// 两种等效方式
ByteBuf buf = PooledByteBufAllocator.DEFAULT.buffer(1024);
ByteBuf buf = ctx.alloc().buffer(1024); // ctx.alloc() 默认返回 PooledByteBufAllocatorUnpooledByteBufAllocator:
每次都分配新内存,用完释放。适合内存使用量不可预测的场景(避免内存池占用过多固定内存)。
PreferHeapByteBufAllocator:
包装另一个分配器,优先分配堆内内存。
6.2 在 ServerBootstrap 中配置分配器
// 为所有新建的 Channel 配置 ByteBufAllocator
serverBootstrap
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);也可以在 Handler 中通过 ctx.alloc() 获取当前 Channel 配置的分配器,这是在 Handler 中创建 ByteBuf 的推荐方式(而非直接使用 Unpooled 工具类):
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// 使用当前 Channel 配置的 allocator(通常是 PooledByteBufAllocator)
ByteBuf response = ctx.alloc().buffer(1024);
try {
buildResponse(response, (ByteBuf) msg);
ctx.writeAndFlush(response);
response = null;
} finally {
if (response != null) response.release();
}
}第 7 章 ByteBuf 使用规范总结
7.1 核心使用规范
将 ByteBuf 的核心使用规范总结为以下几点,是生产代码中防止内存泄漏的关键:
规范一:谁创建谁负责最终释放(或传递责任)
// 在 channelRead 中,如果消费了 ByteBuf(不往下传),必须释放
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = (ByteBuf) msg;
try {
// 处理数据...
} finally {
buf.release(); // 必须释放!
}
}
// 如果往下传,释放责任转移给下游
ctx.fireChannelRead(msg); // 不在这里释放规范二:优先使用 SimpleChannelInboundHandler
在 Handler 只处理某种特定类型的消息时,继承 SimpleChannelInboundHandler,自动处理引用计数:
// 推荐:自动释放
public class MyHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) {
// 在这里放心使用 msg,不需要手动释放
// SimpleChannelInboundHandler 在此方法返回后自动 release
}
}规范三:writeAndFlush 之后不要再 release
ByteBuf response = buildResponse(ctx);
ctx.writeAndFlush(response);
// 正确:writeAndFlush 会在写完后自动释放 response
// 错误:response.release(); // 不要这样做!会导致 refCnt < 0规范四:slice/duplicate 后,传出范围前先 retain
ByteBuf slice = originalBuf.slice(0, headerSize);
// 如果 slice 传给其他 Handler 或跨越原 ByteBuf 的生命周期
slice.retain(); // 增加引用计数
ctx.fireChannelRead(slice);
// originalBuf 可以正常 release,slice 的内存安全由其自己的引用计数保证
originalBuf.release();规范五:异常路径同样需要释放
ByteBuf buf = ctx.alloc().buffer();
try {
doSomethingThatMayThrow(buf);
ctx.writeAndFlush(buf);
buf = null; // 写出成功,所有权转移给 writeAndFlush
} catch (Exception e) {
if (buf != null) {
buf.release(); // 异常时手动释放
}
ctx.fireExceptionCaught(e);
}总结
ByteBuf 是 Netty 高性能的关键支柱之一,其设计在 JDK ByteBuffer 的基础上做出了三项根本性改进:
-
读写双指针(
readerIndex/writerIndex):彻底消除了flip()/compact()的使用负担,读写操作独立、直观; -
引用计数:为堆外内存的生命周期管理提供了精确的控制机制,通过
retain()/release()明确所有权,配合ResourceLeakDetector实现内存泄漏的及时发现;规则是”谁持有谁负责”,SimpleChannelInboundHandler和writeAndFlush()都内置了自动释放; -
池化内存(
PooledByteBufAllocator):通过 jemalloc 算法复用内存块,避免高并发下频繁的堆外内存分配/释放,极大减轻 GC 压力;配合CompositeByteBuf、slice()和FileRegion三种零拷贝技术,最大化减少内存拷贝开销; -
五种分类(堆内/堆外 × 池化/非池化 + CompositeByteBuf)覆盖不同场景,默认使用池化堆外内存(
PooledByteBufAllocator.DEFAULT.directBuffer())以获得最优 I/O 性能。
下一篇深入 ChannelPipeline 的设计精髓——双向链表结构、InboundHandler 与 OutboundHandler 的传播机制、以及 ChannelHandlerContext 在链路传播中的核心作用:05 ChannelPipeline与ChannelHandler——责任链模式的精妙设计。
参考资料
io.netty.buffer.ByteBuf源码io.netty.buffer.PooledByteBufAllocator源码io.netty.buffer.CompositeByteBuf源码io.netty.util.ResourceLeakDetector源码- Norman Maurer,《Netty in Action》第 5 章 ByteBuf
思考题
- ChannelPipeline 中的 InboundHandler 从 Head 到 Tail 正序执行,OutboundHandler 从 Tail 到 Head 逆序执行。如果在 Pipeline 中先添加 InboundHandler A,再添加 InboundHandler B,入站数据的处理顺序是 A → B。但如果 A 中调用了
ctx.fireChannelRead(msg)和ctx.channel().writeAndFlush(resp),后者会从 Tail 开始还是从 A 的位置开始执行出站 Handler?ChannelHandlerContext.write()和Channel.write()的行为不同——前者从当前 Handler 的位置开始向前(Outbound 方向)传播,后者从 Pipeline 的 Tail 开始传播。在什么场景下使用ctx.write()比channel().write()更高效?如果一个 OutboundHandler 在write()方法中调用了ctx.channel().write(),会发生无限循环吗?- Netty 允许在运行时动态添加和移除 ChannelHandler(如 SSL HandshakeHandler 在握手完成后移除自己)。在 Pipeline 修改期间,如果有数据正在流经 Pipeline,是否存在并发安全问题?Netty 是如何保证 Pipeline 修改的线程安全性的?