# BufferAllocator中的池化
从整体来说,分配缓冲区的时候Netty利用了缓存机制,尽最大可能复用重复的内存,并减少内存碎片。
Netty的池化通过PoolChunk来完成,可以认为每次申请Buffer时,都是通过某个PoolChunk中的共享内存来分配的。具体策略如下:
# PoolArena
对于PoolArena来说,其内部按照6级分位来划分内存空间。每一级单独维护缓存。
q100 = new PoolChunkList(this, null, 100, Integer.MAX_VALUE, chunkSize);
q075 = new PoolChunkList(this, q100, 75, 100, chunkSize);
q050 = new PoolChunkList(this, q075, 50, 100, chunkSize);
q025 = new PoolChunkList(this, q050, 25, 75, chunkSize);
q000 = new PoolChunkList(this, q025, 1, 50, chunkSize);
qInit = new PoolChunkList(this, q000, Integer.MIN_VALUE, 25, chunkSize);
6个chunkList形成一个双向链表,里面对应的内存分片会根据当前占用的空间大小进行动态调整。
q025中25和75的含义是,当前chunkList分配的单片最多占用75%的的chunkSize,最少占用25%的chunkSize。对于不满足的场景,会顺延到对应的ChunkList中。
- 每次申请内存时,从head(init)开始申请,由于init中的最大内存量是很少的,因此如果申请失败,就转而到下一个(25)中申请。如果申请成功了,对应的chunk会再次检查自己是否还满足当前百分位,如果不满足,那么自动挪到对应的list中。
- 每次释放内存时,和申请一样在释放后进行调整。
这样做可以保证每次都先从内存空间最大的chunk中申请内存,来保证空间利用率的最大化。
# PoolChunk
PoolChunk是PoolArena中的最小单元。
Chunk内部也有如下概念:
- page表示内存分片的最小单位。
- run表示page集合。
- chunk表示run集合。
- chunkSize=maxPages * pageSize
每次Buffer分配的时候,从上到下寻找第一个可分配的空间。
A chunk has the following layout:
/-----------------\
| run |
| |
| |
|-----------------|
| run |
| |
|-----------------|
| unalloctated |
| (freed) |
| |
|-----------------|
| subpage |
|-----------------|
| unallocated |
| (freed) |
| ... |
| ... |
| ... |
| |
| |
| |
\-----------------/
# 内存申请
申请策略保存在PoolArena的父类SizeClasses。
可以理解为是一个基于分组的内存分配策略。
sizeClasses中的内存分配,可以理解为每个分组都是上一个分组的2^n倍。因此group大小按照log方式保存。
这样的好处在于对于任意size的申请来说,首先可以通过这个表格对照来计算出最佳的大小和类型(subPage或run)。
* sizeClasses: Complete table of [index, log2Group, log2Delta, nDelta, isMultiPageSize,
* isSubPage, log2DeltaLookup] tuples.
* index: Size class index.
* log2Group: Log of group base size (no deltas added).
* log2Delta: Log of delta to previous size class.
* nDelta: Delta multiplier.
* isMultiPageSize: 'yes' if a multiple of the page size, 'no' otherwise.
* isSubPage: 'yes' if a subpage size class, 'no' otherwise.
* log2DeltaLookup: Same as log2Delta if a lookup table size class, 'no'
* otherwise.
* (index, log2Group, log2Delta, nDelta, isMultiPageSize, isSubPage, log2DeltaLookup)
* <p>
* ( 0, 4, 4, 0, no, yes, 4)
* ( 1, 4, 4, 1, no, yes, 4)
* ( 2, 4, 4, 2, no, yes, 4)
* ( 3, 4, 4, 3, no, yes, 4)
* <p>
* ( 4, 6, 4, 1, no, yes, 4)
* ( 5, 6, 4, 2, no, yes, 4)
* ( 6, 6, 4, 3, no, yes, 4)
* ( 7, 6, 4, 4, no, yes, 4)
* <p>
* ( 8, 7, 5, 1, no, yes, 5)
* ( 9, 7, 5, 2, no, yes, 5)
* ( 10, 7, 5, 3, no, yes, 5)
* ( 11, 7, 5, 4, no, yes, 5)
* ...
* ...
* ( 72, 23, 21, 1, yes, no, no)
* ( 73, 23, 21, 2, yes, no, no)
* ( 74, 23, 21, 3, yes, no, no)
* ( 75, 23, 21, 4, yes, no, no)
* <p>
* ( 76, 24, 22, 1, yes, no, no)
# 数据结构
handle:
是一个长数字,bit抽象成如下:
oooooooo ooooooos ssssssss ssssssue bbbbbbbb bbbbbbbb bbbbbbbb bbbbbbbb
- o:15bit,表示chunk中的page偏移量。
- s:15bit,表示当前run中的page数量。
- u:1bit,表示是否被使用。
- e:1bit,表示是否是subPage。
- b:32bit,表示subPage的bitmapIdx。如果不是subPage就是0.
runsAvailMap:
- key: runOffset
- value: handle
runsAvail:
优先队列的列表。每个优先队列管理相同大小的run。run通过offset设置优先级,所以每次都可以分配最小offset的run。
# 算法
allocateRun(size):
- 找到第一个满足要求的run
- 如果run的page比需要的多,run分裂,分裂后面的留给后续使用。
allocateSubpage(size):
- 找到第一个满足size没满的subpage。如果找到了直接返回,否则分配一个新的PoolSubpage。
- 执行SubPage的分配(allocate)。
free(handle, len, buffer)
- 如果释放的是subpage,那么释放当前标记。
- 如果subpage没被使用,或者是一个run,那么释放。
- 合并连续的可用run。
- 保存合并后的run。