【Netty源码】ByteBuf源码剖析
来源:互联网 发布:渝商创投网络借贷 编辑:程序博客网 时间:2024/04/28 09:43
ByteBuf概述
1.ByteBuf与ByteBuffer的区别
ByteBuf不是对ByteBuffer的封装,而是重新实现了一个缓冲区。ByteBuffer只使用了一个position指针来记录当前的读写位置,ByteBuf使用了两个指针readerIndex, writerIndex分别来记录当前的读写位置,使用起来更加简单和方便。
ByteBuffer是一个固定长度的缓冲区,当put方法要写的数据大于可写的容量时会抛出异常。ByteBuf改进了这个设计,支持自动扩容。每次put之前会检查是否可以完全写入,如果不能,就会自动扩展ByteBuf的容量,保证put方法不会抛出异常。
和ByteBuffer一样,ByteBuf也支持堆内缓冲区和堆外直接缓冲区,根据经验来说,底层IO处理线程的缓冲区使用堆外直接缓冲区,减少一次IO复制。业务消息的编解码使用堆内缓冲区,分配效率更高,而且不涉及到内核缓冲区的复制问题。
ByteBuf的堆内缓冲区又分为内存池缓冲区PooledByteBuf和普通内存缓冲区UnpooledHeapByteBuf。PooledByteBuf采用二叉树来实现一个内存池,集中管理内存的分配和释放,不用每次使用都新建一个缓冲区对象。UnpooledHeapByteBuf每次都会新建一个缓冲区对象。在高并发的情况下推荐使用PooledByteBuf,可以节约内存的分配。在性能能够保证的情况下,可以使用UnpooledHeapByteBuf,实现比较简单
2.ByteBuf的特点
- 可以自定义缓冲类型;
- 通过内置的复合缓冲类型,实现零拷贝(zero-copy);
- 不需要调用flip()来切换读/写模式;
- 读取和写入索引分开;
- 方法链;
- 引用计数;
- Pooling(池)。
3.实现机制
ByteBuf实际上是在一个抽象的字节数组byte[]上进行读/写操作的集合。它提供了两个指针变量用来支持读写操作:readerIndex和writerIndex。
在对象初始化的时候,readerIndex和writerIndex的值为0,随着读操作和写操作的进行,writerIndex和readerIndex都会增加,不过readerIndex不能超过writerIndex,
在进行读取操作之后,0到readerIndex之间的空间会被视为discard,调用ByteBuf的discardReadBytes方法,可以对这部分空间进行释放重用,类似于ByteBuffer的compact操作,对缓冲区进行压缩
eaderIndex到writerIndex的空间,相当于ByteBuffer的position到limit的空间,可以对其进行读取,WriterIndex到capacity的空间,则相当于ByteBuffer的limit到capacity的空间,是可以继续写入的
分析:由上图可知,ByteBuf真正可读取的内容长度是writerIndex - readerIndex。
ByteBuf的常见操作
1.readBytes
public ByteBuf readBytes(ByteBuf dst, int dstIndex, int length) { checkReadableBytes(length); getBytes(readerIndex, dst, dstIndex, length); readerIndex += length; return this;}protected final void checkReadableBytes(int minimumReadableBytes) { ensureAccessible(); if (minimumReadableBytes < 0) { throw new IllegalArgumentException("minimumReadableBytes: " + minimumReadableBytes + " (expected: >= 0)"); } if (readerIndex > writerIndex - minimumReadableBytes) { throw new IndexOutOfBoundsException(String.format( "readerIndex(%d) + length(%d) exceeds writerIndex(%d): %s", readerIndex, minimumReadableBytes, writerIndex, this)); }}
分析:
在进行读操作之前,首先对缓冲区可用的空间进行校验。如果要读取的字节长度小于0,就会抛出IllegalArgumentException异常,如果要读取的字节长度大于已写入的字节长度,会抛出IndexOutOfBoundsException异常。
通过校验之后,调用getBytes方法,从当前的readerIndex开始,读取length长度的字节数据到目标dst中,由于不同的子类实现不一样,getBytes是个抽象方法,由对应的子类去实现。如果读取数据成功,readerIndex将会增加相应的length。
2.writeBytes
public ByteBuf writeBytes(byte[] src, int srcIndex, int length) { ensureWritable(length); setBytes(writerIndex, src, srcIndex, length); writerIndex += length; return this;}public ByteBuf ensureWritable(int minWritableBytes) { if (minWritableBytes < 0) { throw new IllegalArgumentException(String.format( "minWritableBytes: %d (expected: >= 0)", minWritableBytes)); } if (minWritableBytes <= writableBytes()) { return this; } if (minWritableBytes > maxCapacity - writerIndex) { throw new IndexOutOfBoundsException(String.format( "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s", writerIndex, minWritableBytes, maxCapacity, this)); } // Normalize the current capacity to the power of 2. int newCapacity = calculateNewCapacity(writerIndex + minWritableBytes); // Adjust to the new capacity. capacity(newCapacity); return this;}private int calculateNewCapacity(int minNewCapacity) { final int maxCapacity = this.maxCapacity; final int threshold = 1048576 * 4; // 4 MiB page if (minNewCapacity == threshold) { return threshold; } // If over threshold, do not double but just increase by threshold. if (minNewCapacity > threshold) { int newCapacity = minNewCapacity / threshold * threshold; if (newCapacity > maxCapacity - threshold) { newCapacity = maxCapacity; } else { newCapacity += threshold; } return newCapacity; } // Not over threshold. Double up to 4 MiB, starting from 64. int newCapacity = 64; while (newCapacity < minNewCapacity) { newCapacity <<= 1; } return Math.min(newCapacity, maxCapacity);}//UnpooledHeapByteBuf的capacity实现public ByteBuf capacity(int newCapacity) { ensureAccessible(); if (newCapacity < 0 || newCapacity > maxCapacity()) { throw new IllegalArgumentException("newCapacity: " + newCapacity); } int oldCapacity = array.length; if (newCapacity > oldCapacity) { byte[] newArray = new byte[newCapacity]; System.arraycopy(array, 0, newArray, 0, array.length); setArray(newArray); } else if (newCapacity < oldCapacity) { byte[] newArray = new byte[newCapacity]; int readerIndex = readerIndex(); if (readerIndex < newCapacity) { int writerIndex = writerIndex(); if (writerIndex > newCapacity) { writerIndex(writerIndex = newCapacity); } System.arraycopy(array, readerIndex, newArray, readerIndex, writerIndex - readerIndex); } else { setIndex(newCapacity, newCapacity); } setArray(newArray); } return this;}
分析:
读操作是将源字节数组从srcIndex开始,length长度的数据写入到当前的ByteBuf中的。 一开始需要对写入数组的字节数进行校验,如果写入长度小于0,将会抛出IllegalArgumentException异常,
如果写入字节数小于当前ByteBuf的可写入字节数,则通过检验。如果写入字节数大于缓冲区最大可动态扩展的容量maxCapacity,就会抛出IndexOutOfBoundsException异常,否则的话,就会通过动态扩展来满足写入需要的字节数。
首先通过calculateNewCapacity计算出重新扩展后的容量,然后调用capacity方法进行扩展,不同的子类有不同实现,所以也是一个抽象方法。
计算扩展容量,首先设置门阀值为4m,如果要扩展的容量等于阀值就使用阀值作为缓冲区新的容量,如果大于阀值就以4M作为步长,每次增加4M,如果扩展期间,要扩展的容量比最大可扩展容量还大的话,就以最大可扩展容量maxCapacity为新的容量。否则的话,就从64开始倍增,直到倍增之后的结果大于要扩展的容量,再把结果作为缓冲区的新容量。
通过先倍增再步长来扩展容量,如果我们只是writerIndex+length的值作为缓冲区的新容量,那么再以后进行写操作的时候,每次都需要进行容量扩展,容量扩展的过程需要进行内存复制,过多内存复制会导致系统的性能下降,之所以是倍增再部长,在最初空间比较小的时候,倍增操作并不会带来太多的内存浪费,但是内存增长到一定的时候,再进行倍增的时候,就会对内存造成浪费,因此,需要设定一个阀值,到达阀值之后就通过步长的方法进行平滑的增长。
3.clear
public ByteBuf clear() { readerIndex = writerIndex = 0; return this;}
分析:clear操作只是把readerIndex和writerIndex设置为0,不会对存储的数据进行修改。
4.discardReadBytes
public ByteBuf discardReadBytes() { ensureAccessible(); if (readerIndex == 0) { return this; } if (readerIndex != writerIndex) { setBytes(0, this, readerIndex, writerIndex - readerIndex); writerIndex -= readerIndex; adjustMarkers(readerIndex); readerIndex = 0; } else { adjustMarkers(readerIndex); writerIndex = readerIndex = 0; } return this;}protected final void adjustMarkers(int decrement) { int markedReaderIndex = this.markedReaderIndex; if (markedReaderIndex <= decrement) { this.markedReaderIndex = 0; int markedWriterIndex = this.markedWriterIndex; if (markedWriterIndex <= decrement) { this.markedWriterIndex = 0; } else { this.markedWriterIndex = markedWriterIndex - decrement; } } else { this.markedReaderIndex = markedReaderIndex - decrement; markedWriterIndex -= decrement; }}
分析:
可以通过discardReadByte方法去重用已经读取过的缓冲区。
首先对readerIndex进行判断:
- 如果readerIndex等于0,就说明没有读取数据,没有可以用来重用的空间,直接返回;
- 如果readerIndex大于0且不等于writerIndex的话,说明有进行数据读取被丢弃的缓冲区,也有还没有被读取的缓冲区。调用setBytes方法进行字节数组的复制,将没被读取的数据移动到缓冲区的起始位置,重新去设置readerIndex和writerIndex,readerIndex为0,writerIndex为原writerIndex-readerIndex;同时,也需要对mark进行重新设置。
首先对markedReaderIndex进行备份然后跟decrement进行比较,如果markedReaderIndex比decrement小的话,markedReaderIndex设置为0,再用markedWriterIndex跟decrement比较,如果小于的话,markedWriterIndex也设置为0,否则的话markedWriterIndex较少decrement;
如果markedReaderIndex比decrement大的话,markedReaderIndex和markedReaderIndex都减去decrement就可以了。
如果readerIndex等于writerIndex的话,说明没有可以进行重用的缓冲区,直接对mark重新设置就可以了,不需要内存复制。
ByteBuf功能类
1.ByteBuf的类关系图
2.AbstractDerivedByteBuf
(1)功能
ByteBuf的抽象基类,实现了包装另一个ByteBuf功能。在AbstractByteBuf的基础上提供了一下功能:
- refCnt:获得该对象的引用计数;
- retain:增加该对象的引用计数(无参数:+1;有参数:+指定的increment);
- release:减少该对象的引用计数(无参数:-1;有参数:-指定的increment),当引用计数减少到0时,释放该对象。返回值为true,当且仅当引用计数变为0和该对象已释放。
- internalNioBuffer:内部实现就是简单的调用nioBuffer(index, length);
- nioBuffer:得到内部buffer的一个区域包装,即得到buffer的子区域作为NIO ByteBuffer,返回的ByteBuffer内容不会再受到原buffer索引或内容改变的影响。
(2)派生类
DuplicatedByteBuf
简单的把所有的数据访问请求发送给内部的buffer。推荐使用ByteBuf.duplicate()来创建该对象,而不是直接调用本身的构造函数。ReadOnlyByteBuf
将原有的ByteBuf包装为制度的ByteBuf,所有的写请求都将被禁止。推荐使用Unpooled.unmodifiableBuffer(ByteBuf)来创建该对象,而不是直接调用本身的构造函数。
对象与内部的buffer使用相同的索引标记SlicedByteBuf
仅暴露内部buffer的一个子区域,即切片。推荐使用ByteBuf.slice()或ByteBuf.slice(int, int)来创建该对象,而不是直接调用本身的构造函数。
3.AbstractReferenceCountedByteBuf
(1)定义
AbstractReferenceCountedByteBuf是ByteBuf实现对引用进行计数的基类,用来跟踪对象的分配和销毁,实现自动内存回收
(2)成员变量
refCntUpdater refCntUpdater是一个AtomicIntegerFieldUpdater类型的成员变量,它可以对成员变量进行原子性更新操作,达到线程安全。
REFCNT_FIELD_OFFSET REFCNT_FIELD_OFFSET是标识refCnt字段在AbstractReferenceCountedByteBuf的内存地址,在UnpooledDirectByteBuf
PooledDirectByteBuf两个子类中都会使用到这个偏移量。
refCnt volatile修饰保证变量的线程可见性,用来跟踪对象的引用次数
(3)对象引用计数器
每调用retain方法一次,引用计数器就会加一。retain方法通过自旋对引用计数器进行加一操作,引用计数器的初始值为1,只要程序是正确执行的话,它的最小值应该为1,当申请和释放次数相等的时候,对应的ByteBuf就会被回收。
当次数为0时,表明对象被错误的引用,就会抛出IllegalReferenceCountException异常,如果次数等于Integer类型的最大值,就会抛出
IllegalReferenceCountException异常。retain通过refCntUpdater的compareAndSet方法进行原子操作更新,compareAndSet会使用获取的值与期望值进行比较,如果在比较器件,有其他线程对变量进行修改,那么比较失败,会再次自旋,获取引用计数器的值再次进行比较,否则的话,就会进行加一操作,退出自旋。
release方法的话与retain方法类似,也是通过自旋循环进行判断和更新,不过当refCnt的值等于1的时候,表明引用计数器的申请跟释放次数一样,对象引用已经不可达了,对象应该要被垃圾收集回收掉了,调用deallocate方法释放ByteBuf对象
retain方法源码如下
@Override public ByteBuf retain() { for (;;) { int refCnt = this.refCnt; final int nextCnt = refCnt + 1; if (nextCnt <= 1) { throw new IllegalReferenceCountException(refCnt, 1); } if (refCntUpdater.compareAndSet(this, refCnt, nextCnt)) { break; } } return this; }
- release方法源码如下
public final boolean release() { for (;;) { int refCnt = this.refCnt; if (refCnt == 0) { throw new IllegalReferenceCountException(0, -1); } if (refCntUpdater.compareAndSet(this, refCnt, refCnt - 1)) { if (refCnt == 1) { deallocate(); return true; } return false; } }}
ByteBuf辅助类
1.ByteBufHolder
- 应用
HTTP协议的请求消息和应答消息都可以携带消息体,这个消息体在NIO ByteBuffer中就是ByteBuffer对象,在Netty中就是ByteBuf对象。而不同的协议消息体中可以含有不同的协议字段和功能,因此需要对ByteBuf进行包装和抽象。
2.ByteBufAllocator
ByteBufAllocator是字节缓冲区分配器,
根据Netty字节缓冲区的实现不同,分为两种不同的分配器PooledByteBufAllocator和UnpooledByteBufAllocator。他们提供了不同ByteBuf的分配方法。
3.CompositeByteBuf
CompositeByteBuf允许将多个ByteBuf的实例组装到一起,形成一个统一的视图。
若某个协议POJO对象包含2部分:消息头和消息体,它们都是ByteBuf对象。当需要对消息进行编码的时候需要进行整合,如果使用JDK的话,有以下2种思路:
- 将某个ByteBuffer复制到另一个ByteBuffer中,或者创建一个新的ByteBuffer
- 通过List等容器,统一维护和处理
4.ByteBufUtil
工具类,提供静态方法用于操作ByteBuf对象。
最有用的是对字符串进行编码和解码
四种内存分配方式
1.类关系图
2.UnpooledHeapByteBuf
(1)定义
UnpooledHeapByteBuf是一个非线程池实现的在堆内存进行内存分配的字节缓冲区,在每次IO操作的都会去创建一个UnpooledHeapByteBuf对象,如果频繁地对内存进行分配或者释放会对性能造成影响。
(2)成员变量
- ByteBufAllocator 用于内存分配
- array 字节数组作为缓冲区,用于存储字节数据
- ByteBuffer 用来实现Netty ByteBuf 到Nio ByteBuffer的变换
(3)动态扩展缓冲区
调用capacity方法动态扩展缓冲区,首先要对扩展容量进行校验,如果新容量的大小小于0或者大于最大可扩展容量maxCapacity的话,抛出IllegalArgumentException异常。
通过校验之后,如果新扩展容量比原来大的话,则创建一个新的容量为新扩展容量的字节数组缓冲区,然后调用System.arraycopy进行内存复制,将旧的数据复制到新数组中去,然后用setArray进行数组替换。动态扩展之后需要原来的视图tmpNioBuffer设置为控。
如果新的容量小于当前缓冲区容量的话,不需要进行动态扩展,但是需要截取部分数据作为子缓冲区。
首先对当前的readerIndex是否小于newCapacity,如果小于的话继续对writerIndex跟newCapacity进行比较,如果writerIndex大于newCapacity的话,就将writerIndex设置为newCapacity,更新完索引之后就通过System.arrayCopy内存复制将当前可读的数据复制到新的缓冲区字节数组中。
如果newCapacity小于readerIndex的话,说明没有新的可读数据要复制到新的字节数组缓冲区中,只需要把writerIndex跟readerIndex都更新为newCapacity既可,最后调用setArray更换字节数组。
3.PooledByteBuf
在Netty4之后加入内存池管理,通过内存池管理比之前ByteBuf的创建性能得到了极大提高。
(1)PoolArena
在内存分配中,为了能够集中管理内存的分配及释放,同时提供分配和释放内存的性能,一般都是会先预先分配一大块连续的内存,不需要重复频繁地进行内存操作,那一大块连续的内存就叫做memory Arena,而PoolArena是Netty的内存池实现类。
在Netty中,PoolArena是由多个Chunk组成的,而每个Chunk则由多个Page组成。PoolArena是由Chunk和Page共同组织和管理的。
(2)PoolChunk
Page 可以用来分配的最小内存块单位
Chunk page的集合
PoolChunk主要负责内存块的分配及释放,chunk中的page会构建成一颗二叉树,默认情况下page的大小是8K,chunk的大小是2^11 page,即16M,构成了11层的二叉树,最下面一层的叶子节点有8192个,与page的数目一样,每一次内存的分配必须保证连续性,方便内存操作。
每个节点会记录自己在Memory Area的偏移地址,当一个节点表示的内存区域被分配之后,那么该节点会被标志为已分配,该节点的所有子节点的内存请求都会忽略。每次内存分配的都是8k(2^n)大小的内存块,当需要分配大小为chunkSize/(2^k)的内存端时,为了找到可用的内存段,会从第K层左边开始寻找可用节点。
(3)PoolSubpage
当对于小于一个Page的内存分配的时候,每个Page会被划分为大小相等的内存块,它的大小是根据第一次申请内存分配的内存块大小来决定的。
一个Page只能分配与第一次内存内存的内存块的大小相等的内存块,如果想要想要申请大小不想等的内存块,只能在新的Page上申请内存分配了。
Page中的存储区域的使用情况是通过一个long数组bitmap来维护的,每一位表示一个区域的占用情况。
4.PooledDirectByteBuf
(1)创建字节缓冲区
由于内存池实现,每次创建字节缓冲区的时候,不是直接new,而是从内存池中去获取,然后设置引用计数器跟读写Index,跟缓冲区最大容量返回。
源码实现
static PooledHeapByteBuf newInstance(int maxCapacity) { PooledHeapByteBuf buf = RECYCLER.get(); buf.reuse(maxCapacity); return buf;}final void reuse(int maxCapacity) { maxCapacity(maxCapacity); setRefCnt(1); setIndex0(0, 0); discardMarks();}
(2)复制字节缓冲区实例
copy方法可以复制一个字节缓冲区实例,与原缓冲区独立。
首先要对index和length进行合法性判断,然后调用PooledByteBufAllocator的directBuffer方法分配一个新的缓冲区。newDirectBuffer方法是一个抽象方法,对于不同的子类有不同的实现。
如果是unpooled的话,会直接创建一个新的缓冲区,如果是pooled的话,它会从内存池中获取一个可用的缓冲区。
源码如下
public ByteBuf copy(int index, int length) { checkIndex(index, length); ByteBuf copy = alloc().directBuffer(length, maxCapacity()); copy.writeBytes(this, index, length); return copy;}public ByteBuf directBuffer(int initialCapacity, int maxCapacity) { if (initialCapacity == 0 && maxCapacity == 0) { return emptyBuf; } validate(initialCapacity, maxCapacity); return newDirectBuffer(initialCapacity, maxCapacity);}// PooledByteBufAllocator protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) { PoolThreadCache cache = threadCache.get(); PoolArena<ByteBuffer> directArena = cache.directArena; ByteBuf buf; if (directArena != null) { buf = directArena.allocate(cache, initialCapacity, maxCapacity); } else { if (PlatformDependent.hasUnsafe()) { buf = new UnpooledUnsafeDirectByteBuf(this, initialCapacity, maxCapacity); } else { buf = new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity); } } return toLeakAwareBuffer(buf);}//UnpooledByteBufAllocatorprotected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) { ByteBuf buf; if (PlatformDependent.hasUnsafe()) { buf = new UnpooledUnsafeDirectByteBuf(this, initialCapacity, maxCapacity); } else { buf = new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity); } return toLeakAwareBuffer(buf);}
5.UnpooledDirectByteBuf
(1)构造方法
protected UnpooledDirectByteBuf(ByteBufAllocator alloc, int initialCapacity, int maxCapacity) { super(maxCapacity); if (alloc == null) { throw new NullPointerException("alloc"); } if (initialCapacity < 0) { throw new IllegalArgumentException("initialCapacity: " + initialCapacity); } if (maxCapacity < 0) { throw new IllegalArgumentException("maxCapacity: " + maxCapacity); } if (initialCapacity > maxCapacity) { throw new IllegalArgumentException(String.format( "initialCapacity(%d) > maxCapacity(%d)", initialCapacity, maxCapacity)); } this.alloc = alloc; setByteBuffer(ByteBuffer.allocateDirect(initialCapacity)); }
(2)capacity方法源码
public ByteBuf capacity(int newCapacity) { ensureAccessible(); // 1 if (newCapacity < 0 || newCapacity > maxCapacity()) { throw new IllegalArgumentException("newCapacity: " + newCapacity); } int readerIndex = readerIndex(); int writerIndex = writerIndex(); int oldCapacity = capacity; if (newCapacity > oldCapacity) { // 2 ByteBuffer oldBuffer = buffer; ByteBuffer newBuffer = allocateDirect(newCapacity); //2.1 oldBuffer.position(0).limit(oldBuffer.capacity()); //2.2 newBuffer.position(0).limit(oldBuffer.capacity()); //2.3 newBuffer.put(oldBuffer); //2.4 newBuffer.clear(); //2.5 setByteBuffer(newBuffer); } else if (newCapacity < oldCapacity) { //3 ByteBuffer oldBuffer = buffer; ByteBuffer newBuffer = allocateDirect(newCapacity); if (readerIndex < newCapacity) { if (writerIndex > newCapacity) { writerIndex(writerIndex = newCapacity); } oldBuffer.position(readerIndex).limit(writerIndex); newBuffer.position(readerIndex).limit(writerIndex); newBuffer.put(oldBuffer); newBuffer.clear(); } else { setIndex(newCapacity, newCapacity); } setByteBuffer(newBuffer); } return this; }
分析:
<1,检测访问性,可达性,就是引用数必须大于0,否则该ByteBuf的内部空间已经被回收了(堆外内存)
<2,扩容操作,思路新建一个缓存区,然后将原先缓存区的数据全部写入到新的缓存区,然后释放旧的缓存区。
<2.1、2.2,申请一个直接缓存区,然后将原缓冲区的postion设置为0,将limit设置为capacity,处于释放状态(从缓存区读)。
<2.3,将新缓存区的postion,limit属性设置为0,老缓存区limit。
<2.4,将原缓冲区写入到新的缓存区,然后将缓存区置的position设置为0,limt设置为capacity,其实这里设置position,capacity的意义不大,因为ByteBuf并不会利用内部的ByteBuffer的limit,postion属性,而是使用readerIndex,wriateIndex。
<2.5,关联新的ByteBuffer,并释放原缓存区的空间。
<3,压缩缓存区。实现思路是新建一个缓存区,如果readerIndex大于新建的ByteBuffer的capacity,则无需将旧的缓存区内容写入到新的缓存区中。如果readerIndex小于新capacity,那需要将readerIndex 至( Math.min(writerIndex, newCapacity) )直接的内容写入到新的缓存,然后释放旧的缓存区。值得注意一点是,如果readerIndex > newCapcity,该ByteBuf的 readerIndex,writerIndex将会被设置为容量值,意味着如果不对readerIndex设置为0,或调用discardReadBytes,该缓存区是不可以使用的,
(3)setByteBuffer方法源码
private void setByteBuffer(ByteBuffer buffer) { ByteBuffer oldBuffer = this.buffer; if (oldBuffer != null) { if (doNotFree) { doNotFree = false; } else { freeDirect(oldBuffer); } } this.buffer = buffer; tmpNioBuf = null; capacity = buffer.remaining(); }
(4)小结
UnpooledDirectByteBuf是一个聚合对象,内部维护了一个java.nio.ByteBuffer的直接对外内存空间,释放UnpooledDirectByteBuf中堆外内存的时机,就是在UnpooledDirectByteBuf被java垃圾回收的时候,应该于此同时需要释放指向的堆外内存,但堆外内存不受JVM GC的管理,所以我们只有感知到UnpooledDirectByteBuf被JVM虚拟机回收后(虚引用),手动去释放堆外内存,
规避内存泄漏
1.为什么要有引用计数器
Netty里四种主要的ByteBuf,
其中UnpooledHeapByteBuf 底下的byte[]能够依赖JVM GC自然回收;而UnpooledDirectByteBuf底下是DirectByteBuffer,除了等JVM GC,最好也能主动进行回收;而PooledHeapByteBuf 和 PooledDirectByteBuf,则必须要主动将用完的byte[]/ByteBuffer放回池里,否则内存就要爆掉。所以,Netty ByteBuf需要在JVM的GC机制之外,有自己的引用计数器和回收过程。
一下又回到了C的冰冷时代,自己malloc对象要自己free。 但和C时代又不完全一样,内有引用计数器,外有JVM的GC,情况更为复杂。
2.引用计数器常识
计数器基于 AtomicIntegerFieldUpdater,因为ByteBuf对象很多,如果都把int包一层AtomicInteger花销较大,而AtomicIntegerFieldUpdater只需要一个全局的静态变量。
所有ByteBuf的引用计数器初始值为1。调用release(),将计数器减1,等于零时, deallocate()被调用,各种回收。
调用retain(),将计数器加1,即使ByteBuf在别的地方被人release()了,在本Class没喊cut之前,不要把它释放掉。
由duplicate(), slice()和order(ByteOrder)所创建的ByteBuf,与原对象共享底下的buffer,也共享引用计数器,所以它们经常需要调用retain()来显示自己的存在。
当引用计数器为0,底下的buffer已被回收,即使ByteBuf对象还在,对它的各种访问操作都会抛出异常。
3.谁来负责Release
在C时代,我们喜欢让malloc和free成对出现,而在Netty里,因为Handler链的存在,ByteBuf经常要传递到下一个Hanlder去而不复还,所以规则变成了谁是最后使用者,谁负责释放。
若出现异常情况,ByteBuf没有成功传递到下一个Hanlder,还在自己地界里的话,一定要进行释放。 谁最后谁负责
InBound Message
假设每一个Handler都把消息往下传,Handler并也不知道谁是启动Netty时所设定的Handler链的最后一员,所以Netty会在Handler链的最末补一个TailHandler,如果此时消息仍然是ReferenceCounted类型就会被release掉。OutBound Message
要发送的消息通常由应用所创建,并调用 ctx.writeAndFlush(msg) 进入Handler链。在每一个Handler中的处理类似InBound Message,最后消息会来到HeadHandler,再经过一轮复杂的调用,在flush完成后终将被release掉。
4.内存泄漏检测
内存泄漏,主要是针对池化的ByteBuf。ByteBuf对象被JVM GC掉之前,没有调用release()去把底下的DirectByteBuffer或byte[]归还到池里,会导致池越来越大。
非池化的ByteBuf,即使像DirectByteBuf那样可能会用到System.gc(),但终归会被release掉的,不会出大事。
本人才疏学浅,若有错,请指出,谢谢!
如果你有更好的建议,可以留言我们一起讨论,共同进步!
衷心的感谢您能耐心的读完本篇博文!
参考链接:堆外内存的回收机制分析
- 【Netty源码】ByteBuf源码剖析
- Netty bytebuf 源码解析
- netty源码分析(二十一)Netty数据容器ByteBuf底层数据结构深度剖析与ReferenceCounted初探
- Netty ByteBuf原理及其源码分析
- netty源码分析 之十一 ByteBuf
- netty源码学习第三章:ByteBuf
- Netty 4.0 源码分析(四):ByteBuf
- netty(十)源码分析之ByteBuf
- Netty源码分析(五)—ByteBuf源码分析
- 深入浅出Netty源码剖析
- 【Netty源码】服务端源码剖析
- 【Netty源码】NioEventLoop源码剖析
- 【Netty源码】ChannelPipeline源码剖析
- Netty之ByteBuf综合剖析
- 【Netty4.X】Netty源码分析之ByteBuf(七)
- netty(十一)源码分析之ByteBuf 二
- netty(十一)源码分析之ByteBuf 三
- netty(十二)源码分析之ByteBuf 四
- androd添加或删除文件后电脑端不能实时更新问题
- LOJ 「SDOI2017」新生舞会(二分 + 分数规划+ 费用流)
- this关键字
- HDU 6063-RXD and math
- 一个数组计算平均值
- 【Netty源码】ByteBuf源码剖析
- linux学习(2)
- FatMouse' Trade
- 数据结构--顺序表查找
- 锁存器搭配译码器控制多个数码管
- Expires、Last-Modified、Etag缓存控制
- Mac下配置Tomcat并在Eclipse添加
- classpath和classpath*的区别
- 20170801 JAVA输出杨辉三角(非等腰三角形)