高性能 Netty 之组件 ByteBuf

1,751 阅读16分钟

前言

一般讲框架的系列文章,都是先讲框架的例子,然后再讲重点的源码解析,后面才是组件拆解,最后是框架的性能与优化点。

接下来的文章,会继续简单分别介绍 Netty 的组件

  • ByteBuf
  • Channel 和 Unsafe
  • ChannelPipeline 和 ChannelHandler
  • EventLoop 和 EventLoopGroup
  • Future 和 Promise

这篇文章讲一下 Netty 的 ByteBuf。但是在讲这篇文章之前,我们必须要讲一下之前 ByteBuf 的前身,或者说是更加原生的实现类 java.nio.ByteBuffer。因为实际上 Netty 的 ByteBuf 与 java.nio.ByteBuffer 是处于等于层面的东西。

Buffer

在讲 ByteBuffer 之前,先讲一些其父类 java.nio.Buffer。Buffer 类其实是一个特定原始类型的数据的容器。换句话说,Buffer 其实就是特定原始类型的元素的线性有限序列。除了装载数据的“内容”属性之外,还包括三个非常重要的参数

  1. capacity,也就是其最大元素数量,
  2. limit,也就是不应该读取或写入的第一个元素的索引
  3. position,指的是下一个要读取或写入的元素的索引

下面我们来根据上面的讲解说说三个参数的代码

public abstract class Buffer {
    private int capacity;		//缓冲区的容量
    private int limit;			//缓冲区的限制
	private int position = 0;	//缓冲区的位置
}

不是说 Buffer 是一个容器类吗?为什么没有容器对象?其实不难猜,Buffer 实际上是作为一个控制位置符号移动来控制容器操作的抽象类,而真正各个装载基本数据类型的容器是在其子类。我们来看看它的子类

  1. ByteBuffer
  2. CharBuffer
  3. DoubleBuffer
  4. FloatBuffer
  5. IntBuffer
  6. LongBuffer
  7. ShortBuffer

例如 ByteBuffer 是字节缓冲区,它是使用 byte[] 进行装载;CharBuffer 是使用 char[] 装载。

那么 Buffer 提供了什么操作呢?

  1. Buffer 提供了数据操作例如 put 和 get
  2. Buffer 提供了容器的标记和重置,例如通过标记一个位置作为记录点,在往后的操作可以通过重置回到记录点,相当于恢复到某个历史点
  3. Buffer 提供了 clear() 方法,作为容器的清除。(将 limit 设为 capacity,并将 position 设置为 0
  4. Buffer 提供了 flip() 方法,为新的通道写入或相对的 get 操作序列做好准备 flip()。(将 limit 设为 position,将 position 设置为 0
  5. Buffer 提供了 rewind() 方法,使缓冲区准备重新读取它已经包含的数据。(保持 limit 不变,将 position 设为 0

上面基本上介绍了 Buffer 的作用,参数以及方法。虽然看似很完美,实际上它还是有缺陷的,例如

  1. 定长,不能动态扩容
  2. 具有可疑危险操作的 API,例如 flip 或 rewind。由于读取或写入,-0,一旦操作失败则是具有可怕的事情发生
  3. API 有限,需要使用者自己来实现更加好的方法

所以 Netty 的 ByteBuf 出现,解决了一些问题。我们继续往下看!

ByteBuf

为了弥补 java.nio.ByteBuf 的不足,Netty 实现了自己 ByteBuffer - ByteBuf。ByteBuf 位于 Netty 的 io.netty.buffer 包下,它实际上也是一个 Byte 数组缓冲区。基本功能上与 java.nio.ByteBuffer 保持一致,但是比 java.nio.ByteBuffer 更为强大。例如说增加了更多更细维度的方法;整合 java.nio.ByteBuffer,通过 Facade 模式进行包装,减少了代码量,降低实现成本。

下面来讲一下 ByteBuf 的实现原理

实现原理

ByteBuf 的实现与 java.nio.ByteBuffer 在于,它仅提供两个指针来实现了字节缓冲区 - readerIndex 和 writerIndex。当我们初始化一个 ByteBuf 的时候,一个缓冲区的样子是这样的

当我们写入数据的时候,writerIndex 会对应进行移动

然后当我们调用“读取数据”的方法的时候,readerIndex 进行移动(这个时候 readerIndex 不能大于 writerIndex)

当调用 discardReadBytes 的时候,writerIndex 会被设置为 readerIndex

当进行 clear 操作的时候,readerIndex 和 writerIndex 都会被置空为 0

整个流程比较简单。相对于 java.nio.ByteBuffer 来说,少了对 capacity 与其他标志的互动。

功能介绍

对于 ByteBuf 的功能介绍,如下

  1. 顺序读操作
  2. 顺序写操作
  3. 动态扩容
  4. readerIndex 和 writerIndex
  5. ReadableBytes 和 Writable bytes
  6. clear 操作
  7. mark 和 rest
  8. 查找操作
  9. Derived bnffers
  10. 转换标准的 ByteBuffer
  11. 随机读写(set 和 get)

顺序读操作

顺序读操作类似于 ByteBuffer 的 get 操作。所谓顺序写就是在可写的第一个下标根据数据类型往后推延。

在 ByteBuf 中,有以下的读方法

带下划线的都是留给子类去实现。这么多方法,我们挑示范性来说 readBytes(byte[], int, int)

    public ByteBuf readBytes(byte[] dst, int dstIndex, int length) {
        checkReadableBytes(length);		//检查可读字节
        getBytes(readerIndex, dst, dstIndex, length);	// 传入 readerIndex,以及目标字节数组,开始下标为 dstIndex,长度为 length
        readerIndex += length;		// readerIndex 增加 length,代表已经读过了
        return this;
    }

里面的 getBytes 方法是留给子类的。如果子类是 UnpooledHeapByteBuf

  public ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length) {
      checkDstIndex(index, length, dstIndex, dst.length);	//查看是否下标越界
      System.arraycopy(array, index, dst, dstIndex, length);	//通过系统复制进行复制
      return this;
  }

如果子类是 UnpooledDirectByteBuf

   void getBytes(int index, byte[] dst, int dstIndex, int length, boolean internal) {
        checkDstIndex(index, length, dstIndex, dst.length);	//检查下标

        ByteBuffer tmpBuf;
        if (internal) {
            tmpBuf = internalNioBuffer();		
        } else {
            tmpBuf = buffer.duplicate(); // 复制一个副本
        }
        // 清除下标并设置 position 为 index,limit 为 index + length
        tmpBuf.clear().position(index).limit(index + length);	
        tmpBuf.get(dst, dstIndex, length);	// 获取范围数据
    }

顺序写操作

写操作与读操作的方法数不相上下,我们挑 write(byte[] byte, int, int) 来讲

    @Override
    public ByteBuf writeBytes(byte[] src, int srcIndex, int length) {
        ensureWritable(length);		//检测可写数量
        setBytes(writerIndex, src, srcIndex, length);		// 写入
        writerIndex += length;		//增加已写的下标
        return this;
    }

我们依旧来看 UnpooledHeapByteBuf 的 setBytes 实现

    public ByteBuf setBytes(int index, byte[] src, int srcIndex, int length) {
        checkSrcIndex(index, length, srcIndex, src.length);
        System.arraycopy(src, srcIndex, array, index, length);
        return this;
    }

再看 UnpooledDirectByteBuf 的 setBytes 实现

    public ByteBuf setBytes(int index, byte[] src, int srcIndex, int length) {
        checkSrcIndex(index, length, srcIndex, src.length);	//检查下标
        ByteBuffer tmpBuf = internalNioBuffer(); 	// 生成一个内部 Nio Buffer
        tmpBuf.clear().position(index).limit(index + length//
        tmpBuf.put(src, srcIndex, length);
        return this;
    }

动态扩容

看完写操作后,有个注意的点。我们之前说,ByteBuf 比 java.nio.ByteBuffer 多出一个好处在于,它能动态扩容,但是为什么代码层面没体现?其实它有一个方法 ensureWritable0 是确保你能够有足够容量写入

  final void ensureWritable0(int minWritableBytes) {
  		//获取可写数据下标
        final int writerIndex = writerIndex();
       	// 查看目标希望的大小容量
        final int targetCapacity = writerIndex + minWritableBytes;
        //如果足够,忽略
        if (targetCapacity <= capacity()) {
            ensureAccessible();
            return;
        }
        //如果 checkBounds 设为 true && targetCapacity 超标了,说明要报错了
        if (checkBounds && targetCapacity > maxCapacity) {
            ensureAccessible();
        }

        // 获取新的容量;
        final int fastWritable = maxFastWritableBytes();
        int newCapacity = fastWritable >= minWritableBytes ? writerIndex + fastWritable
                : alloc().calculateNewCapacity(targetCapacity, maxCapacity);

        // 调整新的容量
        capacity(newCapacity);
    }

maxFastWritableBytes 方法是说返回可以确定写入而不影响到最大字节数内部重新分配或数据复制。如果小于 fastWritable 就直接增加;如果大于 fastWritable 需要重新分配了。

readerIndex 和 writerIndex

经过上面说的,或许你已经知道了 readerIndex 和 writerIndex 其实是在容器上标志目前读的位置以及写的位置。但是 JDK 的 ByteBuffer 却只有一个索引,这 也就是为什么必须调用 flip()方法来在读模式和写模式之间进行切换的原因。

ReadableBytes 和 Writablebytes

ReadableBytes 和 Writablebytes 讲的是对应 readerIndex 和 writerIndex 划分开来的可读Bytes区以及可写Bytes

我们可以发现, 0-readerIndex 组成了丢弃区;readerIndex-writerIndex形成了可读区;writerIndex-capacity形成了可写区

clear 操作

clear 操作非常简单,其实就是重置

    public ByteBuf clear() {
        readerIndex = writerIndex = 0;
        return this;
    }

mark 和 rest

源码里面有两个成员属性

private int markedReaderIndex;//标记读索引
private int markedWriterIndex;//标记写索引

这两个属性可以进行返回上次标记的位置,类似于历史回退作用。例如你预想标记一个读位置,你可以调用 markReaderIndex() 方法,这时候 markedReaderIndex 就是记录这个位置。等到未来的某个节点你想回退,那么可以调用 resetReaderIndex() 方法回到记录的位置。

重用缓冲区

所谓的重用缓冲区,就是讲如何通过 discardsReadBytes 和 discardSomeReadBytes 方法重用已经读取过的缓冲区。我们通过 discardReadBytes 来说明分析

    public ByteBuf discardReadBytes() {
    	//如果 readerIndex 为 0,说明没有重用的缓冲区
        if (readerIndex == 0) {
            ensureAccessible();
            return this;
        }
		//如果 readerIndex 等于 writerIndex 
        if (readerIndex != writerIndex) {
        	// 使用 setBytes 进行字节数组复制
            setBytes(0, this, readerIndex, writerIndex - readerIndex);
            // writerIndex 设置为之前的 writerIndex - readerIndex
            writerIndex -= readerIndex;
            // 最后调整 readerIndex
            adjustMarkers(readerIndex);
            // 调整 readerIndex 为 0 
            readerIndex = 0;
        } else {
        	// 说明没有字节数组可读,不需要进行内存复制,直接调正 mark
            // 将读锁引设置为 0 即可完成缓冲区重用
            ensureAccessible();
            adjustMarkers(readerIndex);
            writerIndex = readerIndex = 0;
        }
        return this;
    }

adjustMarkers 方法主要是根据 readerIndex 来对 markedReaderIndex 和 markedWriterIndex 进行调整

    protected final void adjustMarkers(int decrement) {
        int markedReaderIndex = this.markedReaderIndex;
       	//备份的 markedReaderIndex 和需要减少的 decrement 进行判断
        if (markedReaderIndex <= decrement) {	//小于 decrement
            this.markedReaderIndex = 0;			//将 markedReaderIndex 设为 0
            int markedWriterIndex = this.markedWriterIndex;
            if (markedWriterIndex <= decrement) {	//markedWriterIndex 小于 decrement
                this.markedWriterIndex = 0;		// 设为 0
            } else {
            	// 设为 markedWriterIndex - decrement
                this.markedWriterIndex = markedWriterIndex - decrement;
            }
        } else {
        	// 使用 markedReaderIndex - decrement 作为最新减少的值
            this.markedReaderIndex = markedReaderIndex - decrement;
            markedWriterIndex -= decrement;
        }
    }

skipBytes

有些时候你需要丢弃非法的数据或跳跃不需要读取的字节的时候,可以使用 skipBytes 继续操作。它可以忽略指定长度的数组,读操作时直接跳过这些数据读取后面的缓冲区。实现方式也很简单,如下

    public ByteBuf skipBytes(int length) {
        checkReadableBytes(length);
        readerIndex += length;
        return this;
    }

主要步骤就是检验,以及赋值。

  1. 如果跳跃长度大于 readerUIndex,抛出 IndexOutOfBoundsException
  2. 如果跳跃长度为负数,抛出 IllegalArgumentException 异常
  3. 如果跳跃长度大于 writerIndex,抛出 IndexOutOfBoundsException
  4. 如果合法就从新的 readerUIndex 开始读取数据

源码解析

我们在看 java.nio.ByteBuffer 的时候,我们会发现其实其子类真的多得不得了。

而同时 ByteBuf 的子类同样也是非常多。

所以鉴于文章的篇幅原因我只会挑选较为重要的抽象与实现类来说。下面是 ByteBuf 的实现类

实现的角度上,ByteBuf 分为两种:

  1. 堆内存字节缓冲区。堆内存的特点是内存的分配和回收速度快,可以被 JVM 自动回收;缺点是如果进行 Socket 的 IO 读写,需要额外做一次内存复制,讲堆内存对应的缓冲区复制到内核 Channel 中,性能会有一定的下降。
  2. 直接内存字节缓冲区。非堆内存。它在堆外尽心分配,相比于堆内存,它的分配和回收速度会慢一些,但是它写入或从 SocketChannel 中读取时,由于少了一次内存复制,速度比堆内存快。

这么看来,究竟选用哪一种好?当你使用的时候,可以考虑 ByteBuf 在 IO 通信线程的读写缓冲区使用的 DirectByteBuf;后端业务消息的编解码模式使用 HeapByteBuf,这样根据情况组合达到性能最优。

内存回收的角度上,ByteBuf 分为两种:

  1. 基于对象池的 ByteBuf
  2. 普通的 ByteBuf

基于对象池的 ByteBuf 可以重用 ByteBuf 对象,它自己维护了一个内存池,可以循环利用。提升内存的使用效率,降低由于高负载导致频繁的 GC。虽然基于内存池的 ByteBuf 更加有利于性能提高,但是管理和维护更加复杂,因此 Netty 提供了更加好策略来实现。

AbstractByteBuf 解析

AbstractByteBuf 主要是实现了 ByteBuf 的方法。首先我们从其属性开始入手。

    static final ResourceLeakDetector<ByteBuf> leakDetector =
            ResourceLeakDetectorFactory.instance().newResourceLeakDetector(ByteBuf.class);

    int readerIndex;		// 读索引
    int writerIndex;		// 写索引
    private int markedReaderIndex;	//  mark 读锁引
    private int markedWriterIndex;	//  mark 写索引
    private int maxCapacity;		// 最大容量

在 AbstractByteBuf 中,我们可以发现并没有实现 ByteBuf 缓冲区的实现。这是因为 AbstractByteBuf 并不清楚子类到底是基于堆内存还是直接内存。例如说 UnpooledHeapByteBuf 使用 byte 数组,UnpooledDirectByteBuf 直接使用了 ByteBuffer。即使怎么实现,但是功能是相同的,操作结果也是相同的。

首先是读操作,在 java.nio.ByteBuffer 的实现中,会提供很多实现类来装载不同类型的数据。而在 Netty 中,ByteBuf 就已经可以承担所有基本数据类型的装载了。

AbstractReferenceCountedByteBuf

UnpooledHeapByteBuf

UnpooledHeapByteBuf 基于堆内存进行内存分配的字节缓冲区。我们先看其继承与成员函数

public class UnpooledHeapByteBuf extends AbstractReferenceCountedByteBuf {
    private final ByteBufAllocator alloc;	// 创建 ByteBuf 分配器
    byte[] array;							// 装载数据的容器
    private ByteBuffer tmpNioBuf;			// 为了下面进行 ByteBuffer 转化而准备的
}

我们可以发现,UnpooledHeapByteBuf 实现了一个抽象类 AbstractReferenceCountedByteBuf。

关于动态扩展,主要是再 capacity() 方法。

    public ByteBuf capacity(int newCapacity) {
    	// 检查新库存大小
        checkNewCapacity(newCapacity);
        byte[] oldArray = array;
        int oldCapacity = oldArray.length;
        // 如果相同则不管
        if (newCapacity == oldCapacity) {
            return this;
        }

        int bytesToCopy;
        if (newCapacity > oldCapacity) {
            bytesToCopy = oldCapacity;
        } else {
            trimIndicesToCapacity(newCapacity);
            bytesToCopy = newCapacity;
        }
        //按照新容量分配
        byte[] newArray = allocateArray(newCapacity);
        //将数据 copy 到新的字节数组
        System.arraycopy(oldArray, 0, newArray, 0, bytesToCopy);
        setArray(newArray);		// 设置
        freeArray(oldArray);	// 释放
        return this;
    }

上面的 trimIndicesToCapacity 主要是为了避免扩容给 readerIndex 或 writerIndex 的影响。

    protected final void trimIndicesToCapacity(int newCapacity) {
        if (writerIndex() > newCapacity) {
            setIndex0(Math.min(readerIndex(), newCapacity), newCapacity);
        }
    }

关于转换 JDK 的 ByteBuffer,由于 ByteBuf 是基于 byte 数组实现的,所以 NIO 的 ByteBuffer 的 warp 方法可将 byte 数组转为 ByteBuff 对象。

在 UnpooledHeapByteBuf 的 nioBuffer 方法

    public ByteBuffer nioBuffer(int index, int length) {
        ensureAccessible();
        return ByteBuffer.wrap(array, index, length).slice();
    }

在 ByteBuffer 的 wrap 方法

    public static ByteBuffer wrap(byte[] array, int offset, int length)
    {
        try {
            return new HeapByteBuffer(array, offset, length);
        } catch (IllegalArgumentException x) {
            throw new IndexOutOfBoundsException();
        }
    }

关于子类实现的方法,下面简单列举一下

实现方法说明
isDirect是否是基于堆外内存实现
hasArray是否基于字节数组实现
array如果基于字节数组实现,则返回字节数组成员变量
arrayOffset数组位移
hasMemoryAddress是否有对外内存地址
memoryAddress堆外内存地址
read*读取内容方法
write*写入内容方法

大致上,UnpooledHeapByteBuf 的方法介绍完毕。而与之类似的还有 UnpooledDirectByteBuf,它两实现原理差不多的,区别在于 UnpooledDirectByteBuf 的内部缓冲区是直接使用 java.nio.DirectByteBuffer 的。

PooledByteBuf

PooledByteBuf 是一个基于对象池技术实现的。我们趁着这个基于对象池的技术来说说几个内存池比较重要的类,分别是 PoolArena,PoolChunk,PoolSubpage。它们的关系是一个 PoolArena 可以对应多个 PoolChunk;一个 PoolChunk 可以对应多个 PoolSubpage。

首先是 PoolArena。Memory Arena 是指内存中的一大块连续的区域,PoolArena 就是 Netty 的内存池实现类,也就是内存池对应的一大块连续的区域。为什么需要这么大块连续区域呢?是为了集中管理内存的分配和释放,同时提高分配和释放内存时候的性能。试想,如果有个框架提供了对应分配和释放内存的接口,堆内存的管理被集中到几个类或函数中。这样是不是安全,同时内存也能得到最高效的利用。

但是有点不同在于,Netty 的 PoolArena 是由多个 Chunk 组成的大块内存区域,而每个 Chunk 则由一个或多个 Page 组成。所以,只需要管理和组织 Chunk 和 Page 就可以实现对内存的组织和管理了。PoolArena 中的内存 Chunk 的定义如下

    final PooledByteBufAllocator parent;

    private final int maxOrder;
    final int pageSize;
    final int pageShifts;
    final int chunkSize;
    final int subpageOverflowMask;
    final int numSmallSubpagePools;
    final int directMemoryCacheAlignment;
    final int directMemoryCacheAlignmentMask;
    private final PoolSubpage<T>[] tinySubpagePools;
    private final PoolSubpage<T>[] smallSubpagePools;

    private final PoolChunkList<T> q050;
    private final PoolChunkList<T> q025;
    private final PoolChunkList<T> q000;
    private final PoolChunkList<T> qInit;
    private final PoolChunkList<T> q075;
    private final PoolChunkList<T> q100;

接下来是 PoolChunk。PoolChunk 是用来组织和管理多个 Page 的内存分配和释放。在 Netty 中 Chunk 中的 Page 被构建成为一棵二叉树。假如一个 Chunk 由 16 个 Page 组成,那么它们将会被以图中形式组织起来。

我们可以看到,最底层的 Page 的大小是 4 个字节,Chunk 的大小是 64 个字节。整棵树有 5 层,第一层(也就是叶子节点所在的层)用来分配所有 Page 的内存,第 4 层用来分配 2 个 Page 的内存,以此类推。同时,每个节点都有记录自己在整个 Memory Arena 中的偏移地址,当节点被分配出去后,就会被标记为已分配,自这个节点以下的所有节点在后面的内存分配请求中都会被忽略。同时,对树的遍历方式采用的随机深度优先,并不是一定从最左节点开始的。

最后是 PoolSupage。对于小于一个 Page 的内存,直接可以在 Page 中分配完成。每个 Page 都会切分成大小相等的多个存储块,存储块的大小由第一次申请内存的内存块大小决定。例如一个 Page 可容纳 8 个字节,如果第一次申请 4 个,那么一个 Page 就有两个存储块;如果第一次是 8 个字节,那么这个 Page 被分成 1 个存储块。

PoolSupage 的使用状态使用一个 long 数组来维护。数组中的每个 long 代表每一位表示一个块存储区域的占用情况:0 表示为占用,1 表示已占用。对于一个 4 字节的 Page 来说,如果这个 Page 用来分配 1 个字节的存储区域,那么 long 数组中就只有一个 long 类型的元素,这个数值的低 4 位用来指示各个存储区域的占用情况。对于 128 字节的 Page 来说,如果这个 Page 也是用来分配 1 个字节的存储区域,那么 long 数组就会包含 2 个元素,总共 128 位,每一位代表一个区域的占用情况。下面是 PoolSupage 的变量定义代码。

final class PoolSubpage<T> implements PoolSubpageMetric {
    final PoolChunk<T> chunk;
    private final int memoryMapIdx;
    private final int runOffset;
    private final int pageSize;
    private final long[] bitmap;

    PoolSubpage<T> prev;
    PoolSubpage<T> next;

    boolean doNotDestroy;
    int elemSize;
    private int maxNumElems;
    private int bitmapLength;
    private int nextAvail;
    private int numAvail;
}

上面讲完了 PoolArena PoolChunk PoolSupage,其实它们都是通过状态位标识内存是否可用,不同之处是 Chunk 通过二叉树上对节点进行标识实现,Page 通过维护块的使用状态标识实现。

PooledDirectByteBuf

PooledDirectByteBuf 与 UnPooledDirectByteBuf 差不多,唯一不同是 PooledDirectByteBuf 是基于内存池,而且它们的缓冲区销毁策略不同。也就是说,它们的内存分配策略不同。

创建字节缓冲区实例

因为是基于内存池实现,所以新创建的 PooledDirectByteBuf 对象时不能直接 new 一个对象,而是从内存池获取,然后设置引用计数器的值。

    static PooledDirectByteBuf newInstance(int maxCapacity) {
        PooledDirectByteBuf buf = RECYCLER.get();
        buf.reuse(maxCapacity);
        return buf;
    }

内存池是 PooledDirectByteBuf 的变量 RECYCLER

    private static final ObjectPool<PooledDirectByteBuf> RECYCLER = ObjectPool.newPool(
            new ObjectCreator<PooledDirectByteBuf>() {
        @Override
        public PooledDirectByteBuf newObject(Handle<PooledDirectByteBuf> handle) {
            return new PooledDirectByteBuf(handle, 0);
        }
    });

复制新的字节缓冲区实例

由于是使用了内存池技术实现,所以复制时创建对象不能直接 new 一个实例,而是从内存池拿。所以对应的方法是 copy(int index, int lenght)。

    public ByteBuf copy(int index, int length) {
        checkIndex(index, length);
        ByteBuf copy = alloc().directBuffer(length, maxCapacity());
        return copy.writeBytes(this, index, length);
    }

我们可以发现是通过 allocator 的 directBuffer 方法实例化的。由于 PooledDirectByteBuf 实现了 AbstractByteBufAllocator 抽象类,所以我们看它的 directBuffer 方法。

    public ByteBuf directBuffer(int initialCapacity, int maxCapacity) {
        if (initialCapacity == 0 && maxCapacity == 0) {
            return emptyBuf;
        }
        validate(initialCapacity, maxCapacity);
        return newDirectBuffer(initialCapacity, maxCapacity);
    }

newDirectBuffer 是一个模板模式,由子类进行实现。如果是池化技术,则是

    protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
        PoolThreadCache cache = threadCache.get();
        PoolArena<ByteBuffer> directArena = cache.directArena;

        final ByteBuf buf;
        if (directArena != null) {
            buf = directArena.allocate(cache, initialCapacity, maxCapacity);
        } else {
            buf = PlatformDependent.hasUnsafe() ?
                    UnsafeByteBufUtil.newUnsafeDirectByteBuf(this, initialCapacity, maxCapacity) :
                    new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
        }

        return toLeakAwareBuffer(buf);
    }

如果是非池化技术,则

    protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
        final ByteBuf buf;
        if (PlatformDependent.hasUnsafe()) {
            buf = noCleaner ? new InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf(this, initialCapacity, maxCapacity) :
                    new InstrumentedUnpooledUnsafeDirectByteBuf(this, initialCapacity, maxCapacity);
        } else {
            buf = new InstrumentedUnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
        }
        return disableLeakDetector ? buf : toLeakAwareBuffer(buf);
    }

结语

这篇文章主要讲了 java.nio.Buffer 和 ByteBuf 它们之间的不同,以及在 Netty 在 ByteBuf 上的实现和优化。

完!