无题

Netty剖析

1. Rector模型

2. 核心组件

  1. EventLoop:事件循环,处理注册到内核中关注的事件集合
  2. EventLoopGroup:管理EventLoop,读写事件操作时由next()方法进行EventLoop的负载均衡,如接收到一个连接,由EventLoopGroup负载到EventLoop并注册内核事件,后续有关该事件的操作都由该EventLoop操作
  3. Channel:该Channel可以类比于底层的套接字,可以基于该channel进行操作
  4. UnSafe:Channel进行的读写等操作通过Pipeline后,最终进行实际操作的都为该类,并且每一个Channel都有属于自己的Unsafe实现,该类可与底层套接字操作,如注册感兴趣的事件等
  5. Pipeline:每一个Channel都绑定了一个Pipeline,每一个Pipeline里面又包含多个 ChannelHandlerContext
  6. ChannelHandlerContext:该组件封装了ChannelHandler,将各个ChannelHandler封装起来,方便Pipeline构造双向链表
  7. ChannelHandler:实际处理数据的处理程序,包含 ChannelInboundHandler 和 ChannelOutboundHandler
  8. ChannelInboundInvoker 和 ChannelOutboundInvoker:负责触发 ChannelInboundHandler 和 ChannelOutboundHandler的执行

3. EventLoop

可以看做只有一个线程的线程池,由于EventLoopGroup是EventLoop的合集,所以最终任务的执行都会交由EventLoop执行

EventLoop事件循环的启动流程

4. Server端服务Channel接收流程

5. Pipeline

6. ByteBuf

可以看到ByteBuf继承自ReferenceCounted、Comparable、ByteBufConvertible

ReferenceCounted:引用计数对象的接口,该接口规定了引用计数的基本操作,诸如引用计数增加、引用计数释放,Netty该设计为了防止ByteBuffer的内存泄漏

ByteBufConvertible:返回ByteBuf自身的接口

ByteBuf会在写入操作时自动扩容

7. Netty内存管理

  • PoolChunk:Netty向操作系统申请内存的最小单位(默认值4M),是Run的集合
  • Run: 对应一块连续的内存,大小是Page的倍数
  • Page: Chunk的最小分配单元,默认大小为8K,一个Chunk默认有512个Page
  • Subpage: 负责Page内的内存分配,目的是为了减少内存的浪费。如果需要分配的内存小于Page的大小(8K)比如只有100B,如果直接分配一个Page(8K)那就直接浪费了。SubPage可以理解为由一个或多个page组成的,并且可以分配申请内存小于28K的内存

如下为PoolTrunkList 的结构:

如下为PoolArena中的一些结构

Arena只是作为内存分配的工具,底层内存最终的分配通过PoolTrunk实现,而在进行PoolTrunk内存分配的时候,有三个层级的内存分配

概念解读:

可以这样理解PoolArena内存分配的API入口,PoolArena中包含很多的PoolTrunk,而PoolTrunk中又包含很多的run,run是PoolTrunk分配的最小单位

在上面理解中存在PoolSubpeage,可以把PollSubpage也理解为一个run,特别之处在于PoolSubpage是具有分配小内存的run,并且PookSubpage也提供了内存分配接口

在PoolSubpage内部,可以通过标识的bitMap,该数组是long[]类型,每一位表示该PoolSubpage的哪些部分内存已被分配

内存释放过程:

run

run是PoolTrunk分配的最小单位,在进行PooledByteBufAllocator池化内存分配的时候,即创建一个ByteBuf对象,每个PooledByteBuf对象内部都维护着一个long型handle字段,该字段标识着该ByteBuf在在PoolTrunk中的位置,该handle字段释义:

如下面示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
代码片段:
PooledByteBufAllocator pooledByteBufAllocator = PooledByteBufAllocator.DEFAULT;
ByteBuf byteBuf1 = pooledByteBufAllocator.directBuffer(16);
ByteBuf byteBuf2 = pooledByteBufAllocator.directBuffer(16);
ByteBuf byteBuf3 = pooledByteBufAllocator.directBuffer(16);
ByteBuf byteBuf4 = pooledByteBufAllocator.directBuffer(28673);
ByteBuf byteBuf5 = pooledByteBufAllocator.directBuffer(28673);


byteBuf1 : 000000000000000 000000000000001 1 1 00000000000000000000000000000000
byteBuf2 : 000000000000000 000000000000001 1 1 00000000000000000000000000000001
byteBuf3 : 000000000000000 000000000000001 1 1 00000000000000000000000000000010
byteBuf4 : 000000000000001 000000000000100 1 0 00000000000000000000000000000000
byteBuf5 : 000000000000101 000000000000100 1 0 00000000000000000000000000000000

可以看出,对于byteBuf1,由于是首次内存分配,会先创建PoolTrunk,分配的空间为16B,所以小于28K,PoolTrunk分配时创建Subpage,因此run的页偏移为0,该run占用的page数量为1,即8K,标记该

byteBuf1:000000000000000 表示ByteBuf所在的run相对PoolTrunk的偏移为0,

​ 000000000000001 表示该run共占用1个page,即8K

​ 1 表示该run已经被分配

​ 1 表示该run为一个PoolSubpage

​ 00000000000000000000000000000000 表示位于该PoolSubpage的相对元素的偏移为0

byteBuf2:000000000000000 表示ByteBuf所在的run相对PoolTrunk的偏移为0,

​ 000000000000001 表示该run共占用1个page,即8K

​ 1 表示该run已经被分配

​ 1 表示该run为一个PoolSubpage

​ 00000000000000000000000000000001 表示位于该PoolSubpage的相对0位置元素的偏移为1

byteBuf3:000000000000000 表示ByteBuf所在的run相对PoolTrunk的偏移为0,

​ 000000000000001 表示该run共占用1个page,即8K

​ 1 表示该run已经被分配

​ 1 表示该run为一个PoolSubpage

​ 00000000000000000000000000000010 表示位于该PoolSubpage的相对0位置元素的偏移为2

byteBuf4:000000000000001 表示ByteBuf所在的run相对PoolTrunk的偏移为1,即该run是从PoolTrunk的第1个page开始

​ 000000000000100 表示该run共占用1个page,即8K

​ 1 表示该run已经被分配

​ 0 表示该run为一个PoolSubpage

​ 00000000000000000000000000000000 不是PoolSubpage时无意思

byteBuf4:000000000000101 表示ByteBuf所在的run相对PoolTrunk的偏移为5,即该run是从PoolTrunk的第5个page开始

​ 000000000000100 表示该run共占用1个page,即8K

​ 1 表示该run已经被分配

​ 0 表示该run为一个PoolSubpage

​ 00000000000000000000000000000000 不是PoolSubpage时无意思

通过分析发现,对于subpage来说,由于分配时相同尺寸内存都由规定的尺寸分配,所以分配16KB由于偏移为0的run还未存满,所以都是对应同一个值,当分配512次16B之后,该run已经存满,这是再分配16B才会开辟新的run,这是run偏移个规格才会变化

而对于需要分配的内存大于28K来说,如byteBuffer4和byteBuffer5,他们在分配的时候就会以run为单位分配,这个相对更容易理解