Netty 源码解析 -- PoolSubpage 实现原理
前面文章说了 PoolChunk 如何管理 Normal 内存块,本文分享 PoolSubpage 如何管理 Small 内存块。
源码分析基于 Netty 4.1.52
内存管理算法
PoolSubpage 负责管理 Small 内存块。一个 PoolSubpage 中的内存块 size 都相同,该 size 对应 SizeClasses#sizeClasses 表格的一个索引 index。
新创建的 PoolSubpage 都必须加入到 PoolArena#smallSubpagePools[index]链表中。
PoolArena#smallSubpagePools 是一个 PoolSubpage 数组,数组中每个元素都是一个 PoolSubpage 链表,PoolSubpage 之间可以通过 next,prev 组成链表。
感兴趣的同学可以参考《内存对齐类 SizeClasses》。
注意,Small 内存 size 并不一定小于 pageSize(默认为 8K)
默认 Small 内存 size <= 28672(28KB)
关于 Normal 内存块,Small 内存块,pageSize,可参考《PoolChunk 实现原理》。
PoolSubpage 实际上就是 PoolChunk 中的一个 Normal 内存块,大小为其管理的内存块 size 与 pageSize 最小公倍数。
PoolSubpage 使用位图的方式管理内存块。
PoolSubpage#bitmap 是一个 long 数组,其中每个 long 元素上每个 bit 位都可以代表一个内存块是否使用。
内存分配
分配 Small 内存块有两个步骤
PoolChunk 中分配 PoolSubpage。
如果 PoolArena#smallSubpagePools 中已经有对应的 PoolSubpage 缓冲,则不需要该步骤。
PoolSubpage 上分配内存块
PoolChunk#allocateSubpage
#1
这里涉及修改 PoolArena#smallSubpagePools 中的 PoolSubpage 链表,需要同步操作
#2
计算内存块 size 和 pageSize 最小公倍数
#3
分配一个 Normal 内存块,作为 PoolSubpage 的底层内存块,大小为 Small 内存块 size 和 pageSize 最小公倍数
#4
构建 PoolSubpage
runOffset,即 Normal 内存块偏移量,也是该 PoolSubpage 在整个 Chunk 中的偏移量
elemSize,Small 内存块 size
#5
在 subpage 上分配内存块
#1
bitmap 长度为 runSize / 64 / QUANTUM,从《内存对齐类 SizeClasses》可以看到,runSize 都是 2^LOG2_QUANTUM 的倍数。
#2
elemSize:每个内存块的大小
maxNumElems:内存块数量
bitmapLength:bitmap 使用的 long 元素个数,使用 bitmap 中一部分元素足以管理全部内存块。
(maxNumElems & 63) != 0
,代表 maxNumElems 不能整除 64,所以 bitmapLength 要加 1,用于管理余下的内存块。
#3
添加到 PoolSubpage 链表中
前面分析《Netty 内存池与 PoolArena》中说过,在 PoolArena 中分配 Small 内存块时,首先会从 PoolArena#smallSubpagePools 中查找对应的 PoolSubpage。如果找到了,直接从该 PoolSubpage上分配内存。否则,分配一个 Normal 内存块,创建 PoolSubpage,再在上面分配内存块。
PoolSubpage#allocate
#1
没有可用内存块,分配失败。通常 PoolSubpage 分配完成后会从 PoolArena#smallSubpagePools 中移除,不再在该 PoolSubpage 上分配内存,所以一般不会出现这种场景。
#2
获取下一个可用内存块的 bit 下标
#3
设置对应 bit 为 1,即已使用
bitmapIdx >>> 6
,获取该内存块在 bitmap 数组中第 q 元素
bitmapIdx & 63
,获取该内存块是 bitmap 数组中第 q 个元素的第 r 个 bit 位
bitmap[q] |= 1L << r
,将 bitmap 数组中第 q 个元素的第 r 个 bit 位设置为 1,表示已经使用
#4
所有内存块已分配了,则将其从 PoolArena 中移除。
#5
toHandle 转换为最终的 handle
nextAvail 为初始值或 free 时释放的值。
如果 nextAvail 存在,设置为不可用后直接返回该值。
如果不存在,调用 findNextAvail 查找下一个可用内存块。
#1
遍历 bitmap,~bits != 0
,表示存在一个 bit 位不为 1,即存在可用内存块。
#2
遍历 64 个 bit 位,
(bits & 1) == 0
,检查最低 bit 位是否为 0(可用),为 0 则返回 val。
val 等于 (i << 6) | j
,即i * 64 + j
,该 bit 位在 bitmap 中是第几个 bit 位。
bits >>>= 1
,右移一位,处理下一个 bit 位。
内存释放
释放 Small 内存块可能有两个步骤
释放 PoolSubpage 的上内存块
如果 PoolSubpage 中的内存块已全部释放,则从 Chunk 中释放该 PoolSubpage,同时从 PoolArena#smallSubpagePools 移除它。
PoolSubpage#free
#1
将对应 bit 位设置为可以使用
#2
在 PoolSubpage 的内存块全部被使用时,释放了某个内存块,这时重新加入到 PoolArena 中。
#3
未完全释放,即还存在已分配内存块,返回 true
#4
逻辑到这里,是处理所有内存块已经完全释放的场景。
PoolArena#smallSubpagePools 链表组成双向链表,链表中只有 head 和当前 PoolSubpage 时,当前 PoolSubpage 的 prev,next 都指向 head。
这时当前PoolSubpage 是 PoolArena 中该链表最后一个 PoolSubpage,不释放该 PoolSubpage,以便下次申请内存时直接从该 PoolSubpage 上分配。
#5
从 PoolArena 中移除,并返回 false,这时 PoolChunk 会将释放对应 Page 节点。
#1
查找 head 节点,同步
#2
调用 subpage#free 释放 Small 内存块
如果 subpage#free 返回 false,将继续向下执行,这时会释放 PoolSubpage 整个内存块,否则,不释放 PoolSubpage 内存块。
#3
释放 Normal 内存块,就是释放 PoolSubpage 整个内存块。该部分内容可参考《PoolChunk 实现原理》。
如果您觉得本文不错,欢迎关注我的微信公众号,系列文章持续更新中。您的关注是我坚持的动力!
版权声明: 本文为 InfoQ 作者【binecy】的原创文章。
原文链接:【http://xie.infoq.cn/article/59e42d0daa9727e7d501b8478】。文章转载请联系作者。
评论