Netty详解之九:ByteBuf介绍

本篇深入剖析Netty读写缓冲区的设计,内容包括ByteBuf抽象、池化ByteBuf、Direct ByteBuf、Channel的读写冲缓冲区。

ByteBuf

为了提高性能,Netty重新设计了字节缓冲区ByteBuf,类似Nio的ByteBuffer,但工作方式略有区别,比后者更加灵活、高效。

ByteBuf有几个重要属性:

  • capacity:容量;
  • 0:缓冲区开始位置;
  • readIndex:下一个读位置;
  • writeIndex:下一个写位置;

一个ByteBuf对象即可像byte数组一样工作,又可以像IO字节流一样工作。当前的可读数据区是[readIndex,writeIndex);可写区是[writeIndex,capacity);而[0,readIndex)区间的字节是可废弃数据(Discardable),如下图所示:

+-------------------+------------------+------------------+  
| discardable bytes |  readable bytes  |  writable bytes  |  
|                   |     (CONTENT)    |                  |  
+-------------------+------------------+------------------+  
|                   |                  |                  |  
0      <=      readerIndex   <=   writerIndex    <=    capacity

操作方法

要正确使用ByteBuf,必须记住上面的四个属性,以及每个操作对这些属性的影响。

ByteBuf支持的主要操作如下:

方法含义
基本方法
capacity缓冲区能容纳的字节数
readerIndex下一个读位置
writeIndex下一个写位置
capacity(int newCapacity)扩展当前缓冲区至新容量,返回一个容量等于newCapacity的ByteBuf对象(不保证和旧是同一个对象),如果newCapacity小于之前的容量,那么数据可能被截断
maxCapacity此缓冲区能扩充的最大容量,也即上面newCapacity的最大值
ByteBufAllocator alloc()分配此ByteBuf的分配器
unwrap()如果此对象是包装另一个ByteBuf对象生成的,返回后者,否则返回null
isDirect()是否该ByteBuf是一个直接内存缓冲区
isReadOnly()是否只读
asReadOnly()返回该ByteBuf的一个只读版本
readerIndex(int readerIndex)设置readerIndex,不能<0,或>writerIndex
writerIndex(int writerIndex)设置writerIndex,不能<readerIndex,或> capacity
setIndex(int readerIndex, int writerIndex)同时设置readerIndex, writerIndex
readableBytes()=writerIndex-readerIndex
isReadable()=(writerIndex-readerIndex>0)
isReadable(size)=(writerIndex-readerIndex>size)
writableBytes()=capacity-writerIndex
maxWritableBytes()=maxCapacity-writerIndex
maxFastWritableBytes()在不需要执行内存分配、数据copy的前提下,能达到的最大可写字节数,默认等于 writableBytes,实际算法取决于Bytebuf的实现细节
isWritable=(capacity - writerIndex)>0
isWritable(int size)=(capacity - writerIndex)>size
clear()恢复到初始状态,相当于setIndex(0,0)
markReaderIndex&resetReaderIndexmarkReaderIndex记住当前readIndex,resetReaderIndex恢复
markWriterIndex&resetWriterIndex同上
discardReadBytes抛弃已读字节,将[readerIndex, writeIndex)字节迁移到[0, readableBytes),修改readerIndex=0,writeIndex=readableBytes
discardSomeReadBytes抛弃部分已读字节以节省内存,具体数量由实现来决定,以达到最高效
ensureWritable(int minWritableBytes)按需扩展容量,如果writeIndex+minWritableBytes> maxCapacity,抛出IndexOutOfBoundsException
ensureWritable(int minWritableBytes, boolean force)功能同上,但不会抛出异常,参数force影响(writeIndex+minWritableBytes> maxCapacity)条件下的行为:force=true,扩充容量至maxCapacity;force=false,不做扩充。返回值只一个反映操作行为的状态码:=0,容量本来就满足,所以未扩充;=1,容量不足,但未扩充;=2,容量已扩充,满足需求;=3,容量以扩充至maxCapacity,但仍未满足需求。
get方式读数据方法注意:所有的get方法类似byte数组操作,不影响readIndex
getBoolean(int index)get bool值,其他getByte,getShort,getUnsignedByte,getUnsignedShort,getInt,getLong,getChar,getFloat,getDouble功能类似
getShortLE(int index)以Litter Endian的格式获取数据,getUnsignedShortLE,getIntLE,getLongLE,getFloatLE,getDoubleLE类似
getMedium(int index)24bit方式读取int,getMediumLE,getUnsignedMedium,getUnsignedMediumLE
set方式写数据方法注意:所有的set方法类似byte数组操作,不影响writeIndex
setBoolean(int index, boolean value)其他setByte,setShort,setUnsignedByte,setUnsignedShort,setInt,setLong,setChar,setFloat,setDouble功能类似
setShortLE(int index)以Litter Endian的格式获取数据,setUnsignedShortLE,setIntLE,setLongLE,setFloatLE,setDoubleLE类似
setMedium(int index)24bit方式读取int,setMediumLE,setUnsignedMedium,setUnsignedMediumLE
getBytes操作所有getBytes操作不影响本buf的readIndex,writeIndex
getBytes(int index, ByteBuf dst)将[index,writeIndex)区间字节写入dst,该操作增加dst的writeIndex
getBytes(int index, ByteBuf dst, int length)同上,指定写入字节数,而不是默认全部
getBytes(int index, ByteBuf dst, int dstIndex, int length)将[index,index+length)写入目标dst的[dstIndex, dstIndex +length),注意,该操作也不影响dst的readIndex和writeIndex
getBytes(int index, byte[] dst)上面操作的数组版本,其他几个变体都有
getBytes(int index, OutputStream out, int length)目标是stream
getBytes(int index, GatheringByteChannel out, int length)目标是NIO Channel
getBytes(int index, FileChannel out, long position, int length)目标是NIO FileChannel
getCharSequence(int index, int length, Charset charset)读取为字符串
setBytes操作所有getBytes操作不影响本buf的readIndex,writeIndex
setBytes(int index, ByteBuf src)将src可读byte全部写入本buf的index开始的位置,它会增加sr的readIndex。
setBytes其他变体参考getBytes变体
read基本类型read操作增加buf的readInex
readBoolean从readIndex处读一个bool值,readIndex++
readXXXreadByte,readShort,readMedium,readInt等,readIndex增加响应的字节数
write基本类型write操作增加buf的writeInex
write Boolean从writeIndex写入一个bool值,writeIndex++
writeXXXwriteByte,writeShort,writeMedium,writeInt等,writeIndex增加响应的字节数
readBytes操作read操作导致buf的readInex增加读取的字节数
ByteBuf readBytes(int length)读取length字节数,并返回一个新创建的Buf对象,新buf的readIndex=0,writeIndex=length,源buf的readIndex+=length
ByteBuf readBytes(ByteBuf dst)将buf的可读字节写入dst,字节数=min(src.readableBytes,dst.writableBytes),src.readIndex增加,src.writeIndex增加
readBytes(ByteBuf dst, int length)同上,指定长度
readBytes(ByteBuf dst, int dstIndex, int length)同上,但不修改dst.writeIndex
readBytes(byte[] dst)数组版本,也有其他变体
readBytes(ByteBuffer dst)nio buffer版本
readBytes(OutputStream out, int length)ostream版本
readBytes(GatheringByteChannel out, int length)nio channel版本
readCharSequence(int length, Charset charset)字符串版本
readBytes(FileChannel out, long position, int length)文件Channel版本
skipBytes(int length)跳过一些可读字节,readIndex+=length
writeBytes操作write操作导致buf的writeInex增加读取的字节数,支持的操作和readBytes几乎一一对应
字节遍历操作
bytesBefore(int index, int length, byte value)查询value在buf出现的位置,仅限[index,index+length)区间内查找
forEachByte(ByteProcessor processor)遍历可读bytes,其他还有几个变体
buf复制
copy()复制buf的可读字节区,新buf的readIndex=0,新buf和源buf互相独立
copy(int index, int length)新buf的readIndex=0, writeIndex=capacity=length
slice()返回源buf可读字节的一个切片,新buf和源buf共享byte内存,但readIndex,writeIndex互相独立
retainedSlice()相当于slice().retain()
slice(int index, int length)
retainedSlice(int index, int length)
slice变体
duplicate()
retainedDuplicate()
制作buf的一个副本,底层共享byte内存,但readIndex,writeIndex互相独立
nio buffer相关操作ByteBuf是否支持下面某个操作,与具体实现有关
nioBufferCount()buf底层包含的ByteBuffer数量,返回-1,如果该buf不是由ByteBuffer构成的
nioBuffer()返回一个包含可读字节区的ByteBuffer,该ByteBuffer与源ByteBuf是否共享内存与具体实现有关,但readIndex&writeIndex是独立的
nioBuffer(int index, int length)同上,指定字节区,也不是用可读字节区
internalNioBuffer(int index, int length)特定实现支持的接口
nioBuffers()
nioBuffers(int index, int length)
特定实现支持的接口
内存操作
hasArray()buf内部是否被一个byte array支撑
array()返回支撑的byte array,如果不支持,抛出UnsupportedOperationException
arrayOffset()返回buf的0位置,在byte数组中的位置
hasMemoryAddress()buf是否拥有一个指向底层内存的地址
memoryAddress()返回底层内存地址,如果不支持,抛出UnsupportedOperationException
isContiguous()buf时候由一整块内存支持

ByteBuf提供了丰富的操作字节缓冲区方法,即使不用于网络通信,用于其他需要操作字节的场景也相当不错。

AbstractByteBuf

所有的ByteBuf实现都继承自抽象基类AbstractByteBuf,它提供最基础的字段定义和骨架方法实现:

public abstract class AbstractByteBuf extends ByteBuf {
    int readerIndex;
    int writerIndex;
    private int markedReaderIndex;
    private int markedWriterIndex;
    private int maxCapacity;
    
    @Override
    public ByteBuf discardReadBytes() {
        if (readerIndex == 0) {
            return this;
        }
        if (readerIndex != writerIndex) {
            //使用setBytes方法来转移数据
            setBytes(0, this, readerIndex, writerIndex - readerIndex);
            writerIndex -= readerIndex;
            adjustMarkers(readerIndex);
            readerIndex = 0;
        } else {
            adjustMarkers(readerIndex);
            writerIndex = readerIndex = 0;
        }
        return this;
    }
    //其他方法忽略
    ...
 }

AbstractByteBuf是所有ByteBuf实现的基类,它只定义了readerIndex,writerIndex字段及相关操作,并未决定字节数据如何存储;它的方法要么是抽象的,要么是在任意内存方式下都语义正确的实现方式,比如上面discardReadBytes方法内部使用setBytes来迁移数据,子类极有可能提供更高效的实现。

AbstractReferenceCountedByteBuf

AbstractByteBuf的下一层抽象是AbstractReferenceCountedByteBuf,它实现了引用计数逻辑。关于引用计数我们下一章再分析。

ByteBuf的具体实现

ByteBuf有两个维度的特征,第一个是否池化:pooled 或 unpooled,即是否有一个内存池来支持buf的创建和回收,第二个是分配在java堆内存还是堆外内存:heap 或 direct。这样一来,ByteBuf基本上可分为四个大类:UnpooledHeap,UnpooledDirect,PooledHeap,PooledDirect。

UnpooledHeapByteBuf

UnpooledHeapByteBuf是最简单一种ByteBuf实现,它使用java的byte数组来存储数据:

public class UnpooledHeapByteBuf extends AbstractReferenceCountedByteBuf {

	 //分配器
    private final ByteBufAllocator alloc;
    //字节数组
    byte[] array;
    //临时的tmpNioBuf,避免重复计算
    private ByteBuffer tmpNioBuf;

    public UnpooledHeapByteBuf(ByteBufAllocator alloc, int initialCapacity, int maxCapacity) {
        super(maxCapacity);

        if (initialCapacity > maxCapacity) {
            throw new IllegalArgumentException(String.format(
                    "initialCapacity(%d) > maxCapacity(%d)", initialCapacity, maxCapacity));
        }
        this.alloc = checkNotNull(alloc, "alloc");
        setArray(allocateArray(initialCapacity));
        setIndex(0, 0);
    }
    
    protected byte[] allocateArray(int initialCapacity) {
        return new byte[initialCapacity];
    }
    
    //扩充容量,就是重新分配一个byte数组,通过System.arraycopy拷贝数据
    @Override
    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);
        System.arraycopy(oldArray, 0, newArray, 0, bytesToCopy);
        setArray(newArray);
        freeArray(oldArray);
        return this;
    }
    //释放内存,依赖java gc,所以啥都干不了
    protected void freeArray(byte[] array) {
        // NOOP
    }
    
    //使用ByteBuffer.wrap来创建nio buffer,与原ByteBuf共享内存
    @Override
    public ByteBuffer nioBuffer(int index, int length) {
        ensureAccessible();
        return ByteBuffer.wrap(array, index, length).slice();
    }
    
    //释放内存:将array引用清空,让gc来回收byte数组
    protected void deallocate() {
        freeArray(array);
        array = EmptyArrays.EMPTY_BYTES;
    }
 }

基于字节数组buf的操作方法的实现方式是很直观的,熟练的java开发者应该都能编写出来,上面只选取了几个典型的方法实现案例。

UnpooledDirectByteBuf

UnpooledDirectByteBuf接住java nio提供的ByteBuffer来管理堆外内存。

public class UnpooledDirectByteBuf extends AbstractReferenceCountedByteBuf {

    private final ByteBufAllocator alloc;

	 //使用nio ByteBuffer充当数据内存区域
    ByteBuffer buffer; 
    //临时变量
    private ByteBuffer tmpNioBuf;
    private int capacity;
    
    //指当前的buffer不需要手动释放
    private boolean doNotFree;

    public UnpooledDirectByteBuf(ByteBufAllocator alloc, int initialCapacity, int maxCapacity) {
        super(maxCapacity);
        ObjectUtil.checkNotNull(alloc, "alloc");
        checkPositiveOrZero(initialCapacity, "initialCapacity");
        checkPositiveOrZero(maxCapacity, "maxCapacity");
        this.alloc = alloc;
        setByteBuffer(allocateDirect(initialCapacity), false);
    }
    
    //获得新数据内存对象
    //tryFree:是否要释放旧内存
    void setByteBuffer(ByteBuffer buffer, boolean tryFree) {
        if (tryFree) {
            ByteBuffer oldBuffer = this.buffer;
            if (oldBuffer != null) {
                if (doNotFree) {
                    doNotFree = false;
                } else {
                    freeDirect(oldBuffer);
                }
            }
        }

        this.buffer = buffer;
        tmpNioBuf = null;
        capacity = buffer.remaining();
    }
    
    //java nio支持direct内存模式,netty利用了这一点
    protected ByteBuffer allocateDirect(int initialCapacity) {
        return ByteBuffer.allocateDirect(initialCapacity);
    }
    
    //释放内存的方式与平台相关,稍后讨论
    protected void freeDirect(ByteBuffer buffer) {
        PlatformDependent.freeDirectBuffer(buffer);
    }
    
    //读数据使用Nio buffer API
     @Override
    protected byte _getByte(int index) {
        return buffer.get(index);
    }
    
    //写数据使用Nio buffer API
    @Override
    protected void _setByte(int index, int value) {
        buffer.put(index, (byte) value);
    }
    
    //由于内部实现就是nioBuffer,所以返回一个副本即可
    @Override
    public ByteBuffer nioBuffer(int index, int length) {
        checkIndex(index, length);
        return ((ByteBuffer) buffer.duplicate().position(index).limit(index + length)).slice();
    }
    
    //其他方法省略了
    ...

UnpooledDirectByteBuf内部使用了java nio ByteBuffer的direct模式来充当内存结构,所以实现也比较简单。唯一复杂点在于ByteBuffer对外并不提供释放底层数据内存的接口,netty的实现如下:

public class PlatformDependent {
	static {
	     ...
	     
        if (!isAndroid()) {
            if (javaVersion() >= 9) {
                CLEANER = CleanerJava9.isSupported() ? new CleanerJava9() : NOOP;
            } else {
                CLEANER = CleanerJava6.isSupported() ? new CleanerJava6() : NOOP;
            }
        } else {
            CLEANER = NOOP;
        }
        ...
	}
	
	//使用CLEANER来释放内存
	PlatformDependent.freeDirectBuffer(ByteBuffer buffer) {
		CLEANER.freeDirectBuffer(buffer);
	}
}

//现在主流版本是java 8,CLEANER=CleanerJava6
final class CleanerJava6 implements Cleaner {
    private static final long CLEANER_FIELD_OFFSET;
    private static final Method CLEAN_METHOD;
    private static final Field CLEANER_FIELD;

 	 ...
 	 
	 //使用反射方式释放ByteBuffer的内部内存资源
    private static void freeDirectBuffer0(ByteBuffer buffer) throws Exception {
        final Object cleaner;
        if (CLEANER_FIELD_OFFSET == -1) {
            cleaner = CLEANER_FIELD.get(buffer);
        } else {
            cleaner = PlatformDependent0.getObject(buffer, CLEANER_FIELD_OFFSET);
        }
        if (cleaner != null) {
            CLEAN_METHOD.invoke(cleaner);
        }
    }
    ...
}

Netty将direct内存的释放功能抽象为一个叫Cleaner的接口,Cleaner的实现是与平台相关的,在Android上面甚至无法实现。以CleanerJava6为例,它的实现就是一堆的java反射+Unsafe代码(上面仅贴出一点点表达个意思),具体原理是:ByteBuffer内部本有释放内存的方法,但是私有的,通过反射+Unsafe工具找出来执行。

有兴趣的,看看JDK ByteBuffer的代码,和CleanerJava6的实现对照一下就很清楚了。

UnpooledUnsafeNoCleanerDirectByteBuf

Netty还有一种内存管理方式稍微有些区别的UnpooledDirectByteBuf,叫UnpooledUnsafeNoCleanerDirectByteBuf。它命名的意思是:内存并不需要Cleaner来释放,而是通过Unsafe来管理,因为它不是依靠ByteBuffer来分配内存,而是通过Unsafe分配好内存注入到ByteBuffer里面:

class UnpooledUnsafeNoCleanerDirectByteBuf extends UnpooledUnsafeDirectByteBuf {
	 
	 ...
	 
    @Override
    protected ByteBuffer allocateDirect(int initialCapacity) {
        return PlatformDependent.allocateDirectNoCleaner(initialCapacity);
    }
    ...
}

class final class PlatformDependent0 {
    static ByteBuffer allocateDirectNoCleaner(int capacity) {
        return newDirectBuffer(UNSAFE.allocateMemory(Math.max(1, capacity)), capacity);
    }
}

有兴趣可以看看JDK DirectByteBuffer这个类的代码,它有一个接受memoryaddress+capacity的构造方法。

PooledHeapByteBuf

所有Pooled ByteBuf有一个公共基类:

abstract class PooledByteBuf<T> extends AbstractReferenceCountedByteBuf {

	 //负责回收buf对象的回收器
    private final Handle<PooledByteBuf<T>> recyclerHandle;

    //buf内存所在的Chunk
    protected PoolChunk<T> chunk;
    //buf内存所在的位置
    protected long handle;
    //buf内存块引用
    protected T memory;
    //buf内存(相对memory)偏移
    protected int offset;
    //buf内存长度
    protected int length;
    //能扩展的最大长度
    int maxLength;
    //线程局部缓存,能缓存部分ByteBuf,加快分配速度;在这里,回收的时候需要到它
    PoolThreadCache cache;
    
    ByteBuffer tmpNioBuf;
    private ByteBufAllocator allocator;
}

Pooled ByteBuf的实现是比较复杂的,因为底层涉及到内存区域的管理,相当于Netty包含了一个内存分配器;实际上Netty使用基于jemalloc的算法。如果要对Netty池化内存管理机制的进行细节分析,需要单开一个与Netty自身复杂度相当的系列文章,所以这里我们点到即止。

有了上面的基类,PooledHeapByteBuf的实现比较简单:

class PooledHeapByteBuf extends PooledByteBuf<byte[]> {

	 //一个对象池单例
    private static final ObjectPool<PooledHeapByteBuf> RECYCLER = ObjectPool.newPool(
            new ObjectCreator<PooledHeapByteBuf>() {
        @Override
        public PooledHeapByteBuf newObject(Handle<PooledHeapByteBuf> handle) {
            return new PooledHeapByteBuf(handle, 0);
        }
    });

    static PooledHeapByteBuf newInstance(int maxCapacity) {
        PooledHeapByteBuf buf = RECYCLER.get();
        buf.reuse(maxCapacity);
        return buf;
    }
    
    //访问基类分配的内存对象——即byte数组
    protected byte _getByte(int index) {
        return HeapByteBufUtil.getByte(memory, idx(index));
    }
}

PooledDirectByteBuf

PooledDirectByteBuf的实现也是类似的,只是内存对象从byte数组换成了ByteBuffer。

final class PooledDirectByteBuf extends PooledByteBuf<ByteBuffer> {

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

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

    @Override
    protected byte _getByte(int index) {
        return memory.get(idx(index));
    }
}

ByteBufAllocator

在应用中,如果我们要创建一个ByteBuf,不会直接调用构造函数,而是使用对应的ByteBufAllocator。

ByteBufAllocator的抽象定义如下:

public interface ByteBufAllocator {

    ByteBufAllocator DEFAULT = ByteBufUtil.DEFAULT_ALLOCATOR;

	 //分配一个buf,是否pooled,direct由实现来定
    ByteBuf buffer();
    ByteBuf buffer(int initialCapacity);
    ByteBuf buffer(int initialCapacity, int maxCapacity);
    
    //分配一个适合IO操作的buf,一般来说倾向于direct buf
    ByteBuf ioBuffer();
    
    //明确要求分配一个heap buf
    ByteBuf heapBuffer();
    
    //明确要求分配一个direct buf
    ByteBuf directBuffer();
    
    //分配一个组合buf
    CompositeByteBuf compositeBuffer();
    
    boolean isDirectBufferPooled();
    
    //当需要扩充容量至minNewCapacity时,allocator实际采用的新容量
    int calculateNewCapacity(int minNewCapacity, int maxCapacity);
 }

为了缩短篇幅,方法ioBuffer,heapBuffer,directBuffer,compositeBuffer忽略了一些重载版本。CompositeByteBuf是基于组合模式的buf类型,可以组合多个ByteBuf为一个buf。

ByteBufAllocator.DEFAULT是一个全局默认的ByteBufAllocator对象,它取决于两点:java系统属性"io.netty.allocator.type"(=pooled or unpooled),以及平台是否支持直接内存访问。在正常的linux平台下,ByteBufAllocator.DEFAULT默认是PooledByteBufAllocator(direct内存模式)。

UnpooledByteBufAllocator

ByteBufAllocator的实现有pooled和unpooled版本,分配UnpooledByteBuf的allcator就是UnpooledByteBufAllocator。

其成员变量和构造方法如下:

public final class UnpooledByteBufAllocator extends AbstractByteBufAllocator implements ByteBufAllocatorMetricProvider {

	 //记录当前分配的buf总字节数
    private final UnpooledByteBufAllocatorMetric metric = new UnpooledByteBufAllocatorMetric();
    
    //禁止buf泄露检测
    private final boolean disableLeakDetector;
    
    //不是特别明白noCleaner的作用
    private final boolean noCleaner;
    
    public static final UnpooledByteBufAllocator DEFAULT =
            new UnpooledByteBufAllocator(PlatformDependent.directBufferPreferred());
            
    public UnpooledByteBufAllocator(boolean preferDirect) {
        this(preferDirect, false);
    }
    
 	 public UnpooledByteBufAllocator(boolean preferDirect, boolean disableLeakDetector) {
        this(preferDirect, disableLeakDetector, PlatformDependent.useDirectBufferNoCleaner());
    }
            
    public UnpooledByteBufAllocator(boolean preferDirect, boolean disableLeakDetector, boolean tryNoCleaner) {
        super(preferDirect);
        this.disableLeakDetector = disableLeakDetector;
        noCleaner = tryNoCleaner && PlatformDependent.hasUnsafe()
                && PlatformDependent.hasDirectBufferNoCleanerConstructor();
    }
}

UnpooledByteBufAllocator也有一个默认单例,如果没有特殊设置,在桌面系统下参数值disableLeakDetector=true, noCleaner=true。

UnpooledByteBufAllocator分配Buf的方法如下:

//heapBuffer分配
@Override
protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) {
    return PlatformDependent.hasUnsafe() ?
            new InstrumentedUnpooledUnsafeHeapByteBuf(this, initialCapacity, maxCapacity) :
            new InstrumentedUnpooledHeapByteBuf(this, initialCapacity, maxCapacity);
}

//directBuffer分配
@Override
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);
}

但是这里分配的不是前面介绍的4类ByteBuf的任何一种,而是Allocator里面的内部版本。其实这些内部版本并没有什么实质性的功能改变,只是从两个方面增强ByteBuf:

  • Instrumented:这个关键字表示,allocator会追踪内存的分配和释放,记录到UnpooledByteBufAllocatorMetric里面;
  • Unsafe:这个关键字说明,它使用java Unsafe来访问内存数据,而不是通常的byte数组索引或ByteBuffer接口。

具体的代码很繁琐,但信息量不大,就不贴了。

Unpooled

当我们想要使用UnpooledByteBufAllocator时,还可以更方便地,使用Unpooled这个静态工厂类,

public final class Unpooled {

    private static final ByteBufAllocator ALLOC = UnpooledByteBufAllocator.DEFAULT;
    
    //第一组工厂方法
    //创建新ByteBuf
    public static ByteBuf buffer() {
        return ALLOC.heapBuffer();
    }
    public static ByteBuf directBuffer() {
        return ALLOC.directBuffer();
    }
    ...
    
    //第二组工厂方法
    //将byte数组、nio buf、其他byteBuf包装成一个ByteBuf
    //wrappedXXX方法,表示原数据对象与新的ByteBuf共享内存
    public static ByteBuf wrappedBuffer(byte[] array) { ... }
    
    ByteBuf wrappedBuffer(ByteBuf buffer) { ... }
    
    ByteBuf wrappedBuffer(ByteBuf... buffers)  {...}
    
    ByteBuf copiedBuffer(byte[] array) { ... }

    ...
    
    //第三组工厂方法 
    //将byte数组、nio buf、其他byteBuf copy出一个ByteBuf
    //copiedXXX方法,表示原数据对象与新的ByteBuf内存互相独立
    ByteBuf copiedBuffer(byte[]... arrays) {...}
    
    ByteBuf copiedBuffer(ByteBuf buffer) {...}
    
    
    //第四组工厂方法
    //从基本数据类型生成ByteBuf
    ByteBuf  copyInt(int... values) {...}
    
    ByteBuf copyLong(long... values) { ... }
    
    ByteBuf copiedBuffer(CharSequence string, Charset charset) 
    
    //第五组工厂方法  
    //对原ByteBuf进行装饰,已达到特殊目的
     ByteBuf unmodifiableBuffer(ByteBuf buffer) {...}
     
     ByteBuf unreleasableBuffer(ByteBuf buf) {...}
}

上面将Unpooled工厂方法分成五组,方便大家加深印象。

PooledByteBufAllocator

创建池化的ByteBuf使用PooledByteBufAllocator:

public class PooledByteBufAllocator {
	    @Override
    protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) {
        PoolThreadCache cache = threadCache.get();
        PoolArena<byte[]> heapArena = cache.heapArena;

        final ByteBuf buf;
        if (heapArena != null) 
            //此处创建的是PooledUnsafeHeapByteBuf
            buf = heapArena.allocate(cache, initialCapacity, maxCapacity);
        } else {
			 //一般不会走到这个分支
        }
        return toLeakAwareBuffer(buf);
    }

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

        final ByteBuf buf;
        if (directArena != null) {
            //此处创建的是PooledUnsafeDirectByteBuf
            buf = directArena.allocate(cache, initialCapacity, maxCapacity);
        } else {
			 //一般不会走到这个分支
        }
        return toLeakAwareBuffer(buf);
    }
}

PooledByteBufAllocator的初始化过程是设置一大堆内存池管理相关参数,暂时不准备深入这块。

总结

ByteBuf是Netty对数据存储区域的一个抽象,一个ByteBuf有两个维度的特征:Pooled VS Unpooled(池化或非非池化),Heap or Direct(堆内存或堆外内存)。2个维度互相结合,我们有四种ByteBuf的主要类型:UnpooledHeapByteBuf,UnpooledDirectByteBuf,PooledHeapByteBuf,PooledDirectByteBuf。

我们一般不需要直接调用构造函数来创建ByteBuf,而是使用ByteBufAllocator(实际上,Pooled ByteBuf也不可能用构造函数创建)。Netty有PooledByteBufAllocator和UnpooledByteBufAllocator两个allcator实现,都可以创建对应的heap和diret版本。

allcator都有静态单例,我们没有理由另外再创建自己的实例,而对于Unpooled ByteBuf,还可以使用工厂类Unpooled,会更加方便。 ByteBufAllocator接口内也有一个全局的单例allocactor,它是依据平台特征创建的,Channel通信过程默认使用的就是它(在linux平台下,它是PooledByteBufAllocator)。

allcator创建的ByteBuf类型并不与前面提到的四种类型精确匹配,而是他们的子类,一般只是添加了一些装饰性的功能。如果buf类型名包含Instrument关键字,表明allcator能追踪buf的生命周期,从而记录到当前活跃buf的总量;类型名包含Unsafe关键字,表明buf使用java Unsafe来访问内存数据,而不是通常的java接口。

ByteBuf的选择

DirectByteBuf主要特点是用于底层IO操作时速度比较快,如果用于内存计算(字节操作),那么性能反而不如HeapByteBuffer;PooledByteBuf对于小块内存来说,没有什么价值,因为对于小对象的分配和回收,java GC的效率是非常高的,第三方库(包括Netty)很难超越它。

据此,本人有两点心得:

  • 如果对缓冲区执行大量的字节操作,比如基本类型的读写、加密、解析等操作,用Heap ByteBuf更好;
  • 如果是非常小块的内存分配,Unpooled模式更好;

ByteBuf的性能问题非常微妙,netty官方也没有权威说法;我个认为,随着JVM内存管理方面的优化,使用堆外、池化策略是否真的能提高性能,在大多数业务场景下是存疑的。

另外如果使用EpollSocketChannel和KQueueSocketChannel,底层通过JNI来进行socket读写,必须使用direct ByteBuf,因此在Inbound方向,Channelhandler接受到的必然是direct ByteBuf;而在outbound方向,如果我们使用的是非direct ByteBuf,底层会在写入前会制作一份Direct ByteBuf Copy。

在实际项目中,在outbound方向,往往需要对数据对象进行编码,此时选择使用HeapBuf(字节操作更快),还是选择使用DirectBuf(节省一次整体copy),就需要一些测试了。依本人经验,如果在编码时本来就需要使用字节数组,又或者编码过程中需要对每个字节进行某种变换(比如加密计算),那还是使用HeapBuf性能更佳。

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值