Netty笔记(上)

166 阅读7分钟

Netty.buffer:
要点1:
ByteBuf的包装类继承于AbstractDerivedByteBuf,该类有关引用计数的方法都是先通过unwrap()进行解包装后执行
要点2:
AbstractByteBuf在修改readIndex/writeIndex时,会检测是否越界
要点3:
ByteBuf.duplicate() 没有分配新的空间,而是与原buffer共用一个空间,分别维护自己的指针.
要点4:
Slice() 同duplicate()一样, 也是共用一个空间,但是slice的index与原分片的index有一个相对偏移量
要点5:
Copy() 拷贝一个已经存在的bytebuf的数据,但是属于2个空间.
要点6:
byteBuf不支持并发操作,使用者必须自己确保线程安全
要点7:
在没有禁用资源泄露检测的情况下,通过allocator创建的每个directByteBuf都会自动被包装成资源泄露检测对象.
要点8:
PooledByteBuf分配器,会尽可能将arena绑定在某个线程上,以减少竞争,如果arena数小于线程数,那么新线程会寻找此时使用率最低的arena并进行绑定.(从arena分配/归还内存块的行为实际上还是在锁范围内,只是竞争的线程少,性能也就提高了)体现分段锁的思想
要点9:
PoolChunk/PoolSubPage负责内存块的池化,但是不负责PooledByteBuf对象的池化,使用者与PooledByteBuf打交道,PooledByteBuf相当于是内存块的包装, 通过recycle回收和创建PooledByteBuf对象实现资源复用.
要点10:
资源泄露检测的实现:
ByteBuf中有一个api会为每个byteBuf绑定一个leak对象,leak对象继承于WeakReference,当正确管理byteBuf的引用计数时,会通过release进行资源释放(比如堆外内存的释放),同时会关闭leak对象,而当没有被正常回收时,会基于WeakReference的机制被回收,这时会输出有关该buffer的操作记录,便于进行异常排查.
要点11:
在整个内存池化分配的相关组件中,各个组件的职责.
PoolSubPage:
在netty的内存分配算法中每次申请的内存块大小为一个chunk,默认情况下一个chunk为2048个page,chunk的大小为16m,page的大小为8k.然而使用者可能每次需要的内存块还会小于8k. 为了最大限度提高内存利用率,每个page又以固定的大小进一步拆分. 存在2类subPage,一类以16byte为单位每次递增16byte直到达到512byte,总计32种小规格,这种subPage被称为tiny内存块. 另一类以512byte为单位每次递增512直到达到2048byte,总计4种小规格,这种subPage被称为small内存块.8k刚好能被这些规格整除,也就不会有内存浪费了. PoolSubpage就代表以某种小规格进行划分的page内存块.它负责管理自己下面的小内存块哪些已经被使用,哪些可以分配. 在PoolSubPage中还使用了bitmap进行优化,假设使用16byte划分一个page,那么内部总计有512个小内存块.极端情况下前511个都已经被使用,想要获取未使用的内存块就要遍历512次.所以通过位图快速判断某个内存块是否已经分配.bitmap是一个long数组,每个long值能存储64位数据,所以位图以64个内存块为单位将内存块填充到位图中,这样最多只要遍历512/64=8次.

PoolChunk:
内部包含了内存块的分配算法(分配算法不展开说明),基于二叉堆实现,将一个chunk大小的内存块划分成2048个page,在调用allocate后会将内存块,本次申请的内存在内存块中的起始偏移量offset,分配的大小length设置到poolByteBuf中.当申请的内存块大小大于chunk时,被称为huge.这时netty不对申请的内存进行缓存,使用完毕后也会直接释放

PoolByteBuf:
内部包含一个chunk内存块的引用,同时还有标明申请的内存块边界的offset/length. 使用者只能操作边界内的内存块. 当本buf的引用计数归0时,会将本对象存储到对象池(recycle)中.这样下次通过PooledByteBufAllocator创建PoolByteBuf对象就可以复用. 当开启了线程缓存功能时,相关信息会存储在io线程(也就是创建buffer的线程.在netty的线程模型中,经常使用到buffer的就是io线程,就是在网络传输中充当数据的容器)中,这样下次需要分配内存时就可以直接从缓存中获取(当缓存失效时也会将相关的内存块标记成未分配,这样其他线程就可以继续使用空闲内存块).当没有开启缓存时,将直接将相关内存块标记成未分配

PoolChunkList:
在分配内存的过程中,每当分配一个PoolChunk对象后就要加入到PoolChunkList中,PoolChunkList按照不同的内存使用率形成一个链表结构,每一个使用率范围对应的PoolChunkList下又是一组PoolChunk对象,在通过PoolArena申请分配内存时基于内存使用率存在一个使用PoolChunk的优先级关系(此时可能arena中存在多个PoolChunk对象,每个chunk内部已分配的内存量是不同的,体现了内存使用率).

PoolThreadCache:
因为多线程可能会共用PoolArena对象,在分配内存的过程中就会存在线程竞争,PoolThreadCache就是当某线程使用完内存块后优先选择存储到缓存中,这样当下次需要分配等规格大小的内存块时就可以复用缓存中的内存块,避免多线程竞争.注意内部缓存队列使用的是MPSC队列.也就意味着可以在业务线程中释放buffef,它会被缓存到申请buffer的线程,也就是netty的io线程(一般情况下申请内存的总是io线程,这里的线程竞争指的也是io线程间的竞争.但是buffer的释放不一定在本线程,使用者可以将编解码之类的操作隔离到业务线程执行)

PoolArena:
从字面意思上理解竞技场实际上就代表多个线程竞争内存块,它相当于是一个中控对象,负责协调各个组件.这里有一个小技巧,将可能会分配的内存大小划分成不同的队列,这样不同线程在申请相同规格大小的内存块时,锁竞争被细化到规格级别.也是分段锁思想.大体的分配逻辑如下.申请内存时先检测内存规格,之后尝试从本地线程缓存中获取内存.当缓存中没有可用内存块时,根据内存块大小找到匹配的内存块队列,直接申请内存.当可用队列为空时 尝试从已经存在的poolChunk中申请内存,当poolChunk没有足够内存分配时,创建一个新的poolChunk对象

要点12:
AbstractReferenceCountedByteBuf的子类在创建时引用计数已经为1了(实际上是2,使用了一些位运算).所以不需要手动调用retain

Netty.Codec
要点1:
当传输层接收到数据时,每次编解码都需要一个存储临时数据的容器,如果每次都创建新的容器,对象的创建和回收就是一个优化点.如果使用普通的arrayList,无法支持并发访问.最合适的方案还是基于本地线程变量.CodecOutputLists就是针对这种情况产生的,CodecOutputLists和PooledByteBufAllocator一样都是在使用前通过从本地线程变量获取来避免线程间的竞争.但是CodecOutputList,PooledByteBuf的使用本身是不支持并发的,需要使用者自己处理.

要点2:
ByteToMessageCodec定义了粘包拆包的模板,只需要自己定义子类实现decode就可以