本篇深入剖析Netty读写缓冲区的设计,内容包括ByteBuf抽象、池化ByteBuf、Direct ByteBuf、Channel的读写冲缓冲区。
为了提高性能,Netty重新设计了字节缓冲区ByteBuf,类似Nio的ByteBuffer,但工作方式略有区别,比后者更加灵活、高效。
ByteBuf有几个重要属性:
一个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,不能 |
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&resetReaderIndex | markReaderIndex记住当前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++ |
readXXX | readByte,readShort,readMedium,readInt等,readIndex增加响应的字节数 |
write基本类型 | write操作增加buf的writeInex |
write Boolean | 从writeIndex写入一个bool值,writeIndex++ |
writeXXX | writeByte,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提供了丰富的操作字节缓冲区方法,即使不用于网络通信,用于其他需要操作字节的场景也相当不错。
所有的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有两个维度的特征,第一个是否池化:pooled 或 unpooled,即是否有一个内存池来支持buf的创建和回收,第二个是分配在java堆内存还是堆外内存:heap 或 direct。这样一来,ByteBuf基本上可分为四个大类:UnpooledHeap,UnpooledDirect,PooledHeap,PooledDirect。
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接住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的实现对照一下就很清楚了。
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的构造方法。
所有Pooled ByteBuf有一个公共基类:
abstract class PooledByteBuf extends AbstractReferenceCountedByteBuf {
//负责回收buf对象的回收器
private final Handle> recyclerHandle;
//buf内存所在的Chunk
protected PoolChunk 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 {
//一个对象池单例
private static final ObjectPool RECYCLER = ObjectPool.newPool(
new ObjectCreator() {
@Override
public PooledHeapByteBuf newObject(Handle 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的实现也是类似的,只是内存对象从byte数组换成了ByteBuffer。
final class PooledDirectByteBuf extends PooledByteBuf {
private static final ObjectPool RECYCLER = ObjectPool.newPool(
new ObjectCreator() {
@Override
public PooledDirectByteBuf newObject(Handle 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));
}
}
在应用中,如果我们要创建一个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内存模式)。
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:
具体的代码很繁琐,但信息量不大,就不贴了。
当我们想要使用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工厂方法分成五组,方便大家加深印象。
创建池化的ByteBuf使用PooledByteBufAllocator:
public class PooledByteBufAllocator {
@Override
protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) {
PoolThreadCache cache = threadCache.get();
PoolArena 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 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接口。
DirectByteBuf主要特点是用于底层IO操作时速度比较快,如果用于内存计算(字节操作),那么性能反而不如HeapByteBuffer;PooledByteBuf对于小块内存来说,没有什么价值,因为对于小对象的分配和回收,java GC的效率是非常高的,第三方库(包括Netty)很难超越它。
据此,本人有两点心得:
ByteBuf的性能问题非常微妙,netty官方也没有权威说法;我个认为,随着JVM内存管理方面的优化,使用堆外、池化策略是否真的能提高性能,在大多数业务场景下是存疑的。
另外如果使用EpollSocketChannel和KQueueSocketChannel,底层通过JNI来进行socket读写,必须使用direct ByteBuf,因此在Inbound方向,Channelhandler接受到的必然是direct ByteBuf;而在outbound方向,如果我们使用的是非direct ByteBuf,底层会在写入前会制作一份Direct ByteBuf Copy。
在实际项目中,在outbound方向,往往需要对数据对象进行编码,此时选择使用HeapBuf(字节操作更快),还是选择使用DirectBuf(节省一次整体copy),就需要一些测试了。依本人经验,如果在编码时本来就需要使用字节数组,又或者编码过程中需要对每个字节进行某种变换(比如加密计算),那还是使用HeapBuf性能更佳。