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());          // true

Netty 默认使用堆外内存

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 会抛出 IllegalReferenceCountException

3.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() 默认返回 PooledByteBufAllocator

UnpooledByteBufAllocator

每次都分配新内存,用完释放。适合内存使用量不可预测的场景(避免内存池占用过多固定内存)。

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 实现内存泄漏的及时发现;规则是”谁持有谁负责”,SimpleChannelInboundHandlerwriteAndFlush() 都内置了自动释放;

  • 池化内存(PooledByteBufAllocator:通过 jemalloc 算法复用内存块,避免高并发下频繁的堆外内存分配/释放,极大减轻 GC 压力;配合 CompositeByteBufslice()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

思考题

  1. 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?
  2. ChannelHandlerContext.write()Channel.write() 的行为不同——前者从当前 Handler 的位置开始向前(Outbound 方向)传播,后者从 Pipeline 的 Tail 开始传播。在什么场景下使用 ctx.write()channel().write() 更高效?如果一个 OutboundHandler 在 write() 方法中调用了 ctx.channel().write(),会发生无限循环吗?
  3. Netty 允许在运行时动态添加和移除 ChannelHandler(如 SSL HandshakeHandler 在握手完成后移除自己)。在 Pipeline 修改期间,如果有数据正在流经 Pipeline,是否存在并发安全问题?Netty 是如何保证 Pipeline 修改的线程安全性的?