今天进行 PooledByteBuf 源码分析:
PooledByteBuf
是池化的
ByteBuf
,提高了内存分配与释放的速度,它本身是一个抽象泛型类,有三个子类:PooledDirectByteBuf
、
PooledHeapByteBuf
、
PooledUnsafeDirectByteBuf
。三个子类在操作上和其他的 ByteBuf
没有太大的区别,关键在于内存池化技术上。
一、Jemalloc
算法
Netty
的
PooledByteBuf
采用与
jemalloc
一致的内存分配算法。基本思路可用这样的情景类比,想像一下当前电商的配送流程。当顾客采购小件商品(比如书籍)时,直接从同城仓库送出;当顾客采购大件商品(比如电视)时,从区域仓库送出;当顾客采购超大件商品(比如汽车)时,则从全国仓库送出。Netty
的分配算法与此相似。
Netty
中,
Tiny
和
Small
类型的请求都首先从同城仓库(
ThreadCache-tcache
)送出;如果同城仓库没有,则会从区域仓库(PoolArena
)送出,
Normal
类型的请求则从区域仓库(PoolArena
)送出,
Huge
类型的请求则从全国仓库(系统内存)送出。
1、Netty 中规定:
1)
、内存分配的最小单位为
16B
。
2)
、
< 512B
的请求为
Tiny
,
< 512B<X< 8KB(PageSize)
的请求为
Small
,
8KB<=X<=16MB(ChunkSize)的请求为
Normal
,
> 16MB(ChunkSize)
的请求为
Huge
。
3)
、
Tiny
、
Small
、
Normal
、
Huge
中还有细层级,
< Tiny
的请求以
16B
为起点每次增加
16B 作为一个层级,也就是,Tiny 中还有
16B
、
32B
、
48B
、
……480B
、
496B
的层级;
其他的类型则是翻倍:
Small
中还有
512B
、
1KB
、
2KB
、
4KB
的层级;
Normal
中还有
8KB
、
16KB
、
32KB
、
……8MB
、
16MB
的层级;
Huge
中还有
32MB
、
64KB……
的层级。
4
、不管请求的大小,都会将向上规范化,比如:请求分配
511B
、
512B
、
513B
,将依次规范化为 512B
、
512B
、
1KB
。
为了提高内存分配效率并减少内部碎片,
jemalloc
算法将
Arena
切分为小块
Chunk
,根据每块的内存使用率又将小块组合为以下几种状态:QINIT
,
Q0
,
Q25
,
Q50
,
Q75
,
Q100
。Chunk 块可以在这几种状态间随着内存使用率的变化进行转移,内存使用率和状态转移可参 见下图:
其中横轴表示内存使用率(百分比),纵轴表示状态,可以看到:
QINIT
的内存使用率为
[0,25)
、
Q0
为
(0,50)
、
Q100
为
[100,100]
等等。
Chunk
块的初始状态为
QINIT
,当使用率达到
25
时转移到
Q0
状态,再次达到
50
时转移到 Q25
,依次类推直到
Q100
;当内存释放时又从
Q100
转移到
Q75
,直到
Q0
状态且内存使用率为 0
时,该
Chunk
从
Arena
中删除。 像 qInit
、
q000
、
q075
因为本身要维护很多
Chunk
块,所以内部是以链表的形式来组织Chunk 块,同时
qInit
、
q000
、
q075
本身又组织为一个近似的双向链表,如图:
虽然已将
Arena
切分为小块
Chunk
,但实际上
Chunk
是相当大的内存块,在
Netty
默认使用 16MB
。为了进一步提高内存利用率并减少内部碎片,需要继续将
Chunk
切分为小的块Page。一个典型的切分将
Chunk
切分为
2048
块,可知
Page
的大小为:
16MB/2048=8KB
。一个好的内存分配算法,应使得已分配内存块尽可能保持连续,这将大大减少内部碎片,由此jemalloc 使用伙伴分配算法尽可能提高连续性。
伙伴分配算法的基本思想是:我们知道,一个 Chunk 切分为
2048
块
Page
,将这些 Page
作为叶子节点,然后组织起一个满二叉树
然后按层分配满足要求的内存块。
以待分配序列
8KB
、
16KB
、
8KB
为例分析分配过程(每个
Page
大小
8KB
):
8KB--
需要一个
Page
,第
11
层满足要求,故分配
2048
节点即
Page0
;
16KB--
需要两个
Page
,故需要在第
10
层进行分配,而
1024
的子节点
2048
已分配,从左到右找到满足要求的 1025
节点,故分配节点
1025
即
Page2
和
Page3
;
8KB--
需要一个
Page
,第
11
层满足要求,
2048
已分配,从左到右找到
2049
节点即
Page1进行分配。
分配结束后,已分配连续的
Page0-Page3
,这样的连续内存块,大大减少内部碎片并提高内存使用率。
为了实现伙伴算法,
Netty
中使用了
使用两个字节数组
memoryMap
和
depthMap
来表示两棵二叉树,其中
MemoryMap
存放分配信息,depthMap
存放节点的高度信息。
左图表示每个节点的编号,注意从
1
开始,省略
0
是因为这样更容易计算父子关系:子节点加倍,父节点减半,比如 512
的子节点为
1024=512 * 2
。右图表示每个节点的深度,注意从 0
开始。在代表二叉树的数组中,左图中节点上的数字作为数组索引即
id
,右图节点上
的数字作为值。初始状态时,
memoryMap
和
depthMap
相等,可知一个
id
为
512
节点的初始值为 9
memoryMap[512] = depthMap[512] = 9;
depthMap
的值初始化后不再改变,
memoryMap
的值则随着节点分配而改变。当一个节点被分配以后,该节点的值设置为 12
(最大高度
+1
)表示不可用,并且会更新祖先节点的值。下图表示随着 4
号节点分配而更新祖先节点的过程,其中每个节点的第一个数字表示节点编号,第二个数字表示节点高度值。
分配过程如下:
4
号节点被完全分配,将高度值设置为
12
表示不可用。
4
号节点的父亲节点即
2
号节点,将高度值更新为两个子节点的较小值;其他祖先节点亦然,直到高度值更新至根节点。
可推知,
memoryMap
数组的值有如下三种情况:
memoryMap[id] = depthMap[id] --
该节点没有被分配
memoryMap[id] > depthMap[id] --
至少有一个子节点被分配,不能再分配该高度满足的内存,但可以根据实际分配较小一些的内存。比如,上图中分配了 4
号子节点的
2
号节点,值从 1
更新为
2
,表示该节点不能再分配
8MB
的只能最大分配
4MB
内存,因为分配了
4
号节点后只剩下 5
号节点可用。
mempryMap[id] =
最大高度
+ 1
(本例中
12
)
--
该节点及其子节点已被完全分配, 没有剩余空间。
前面我们说过,一个
page
是
8KB
,但是
Netty
又支持
Tiny
、
Small
这种小于
8KB
,最小可达 16B
的内存分配请求,每次都分配一个
page
,很浪费。为了应对这种需求,需要进一步切分 Page
成更小的
SubPage
。
SubPage
是
jemalloc
中内存分配的最小单位,不能再进行切分。SubPage
切分的单位并不固定,以第一次请求分配的大小为单位(最小切分单位为
16B
)。比如,第一次请求分配 32B
,则
Page
按照
32B
均等切分为
256
块;第一次请求
16B
,则
Page 按照 16B
均等切分为
512
块。为了便于内存分配和管理,根据
SubPage
的切分单位进行分组,d 对每个组而言,
Arena
会以双向链表的形式进行管理。那么根据切分的单位的大小和 Page
的大小,
SubPage
分为
2
类:
tinySubpagePools
和 smallSubpagePools,
tinySubpagePools
中的
SubPage
的大小,从
16
字节到
496
个字节,共有 32 个元素,
smallSubpagePools
则有
512
字节、
1024
、
2048
、
4096
,共有
4
个元素。
总的来说,
Arena
中维护的数据结构如下:
在
Arena
数量上,为了减少各个线程进行内存分配时竞争,
Netty
中会有多个
Arena
,默认的数量与处理器的个数有关。线程首次分配内存时,首先会为其分配一个固定的 Arena
。
二、PoolThreadCache
同时在
Netty
中为了提升性能,并不会一开始就从
PoolArena
中分配,因为
Arena
为几个线程共享,而是先从每个线程自己的 PoolThreadCache
中去获取。当然开始的时候,这些Cache 里面都是没有值的,要先从
PoolArena
中获取,当释放
Buf
的时候,才会把之前分配的内存大小放到该 cache
里面,当下次要申请内存的时候,就会先从
PoolThreadCache
中找。
PoolThreadCache
中则维护了
6
个这样的线程缓存区域,
3
个堆内存相关,
3
个直接内存相关,分别对应着三种分配内存的大小。
private final MemoryRegionCache<byte[]>[] tinySubPageHeapCaches;
private final MemoryRegionCache<byte[]>[] smallSubPageHeapCaches;
private final MemoryRegionCache<ByteBuffer>[] tinySubPageDirectCaches;
private final MemoryRegionCache<ByteBuffer>[] smallSubPageDirectCaches;
private final MemoryRegionCache<byte[]>[] normalHeapCaches;
private final MemoryRegionCache<ByteBuffer>[] normalDirectCaches;
执行流程图:
small
类型数组的大小
(
为
4),
而
tiny
、
normal
数组的大小分别分
32
、
3
。
smallSubPageHeapCaches
数组长度为
4
, 依次缓存
[512K, 1024k, 2048k, 4096k]
大小的缓存,
每个的元素对应的缓存
queue
中元素个数不能超过
256
个
;
而
tinySubPageHeapCaches 数组缓存的是[16B, 32B,
…
, 496B]
大小的内存块
,
每个元素对应的缓存
queue
中元素个数不能超过 512
个。
normalHeapCaches
数组结构相同
,
但是只缓存
[8k, 16k, 32k]
大小的内存块
, 每个元素对应的缓存 queue
中元素个数不超过
64
个。
每一个
MemoryRegionCache
中又包含一个队列,队列中的每个元素类型为
Entry
,
Entry 中又包含了一个 PoolChunk
,以方便对内存的管理。
部分源码:
private abstract static class MemoryRegionCache<T> {
private final int size;
private final Queue<Entry<T>> queue;
private final SizeClass sizeClass;
private int allocations;
MemoryRegionCache(int size, SizeClass sizeClass) {
this.size = MathUtil.safeFindNextPositivePowerOfTwo(size);
queue = PlatformDependent.newFixedMpscQueue(this.size);
this.sizeClass = sizeClass;
}
Entry数据结构:
static final class Entry<T> {
final Handle<Entry<?>> recyclerHandle;
PoolChunk<T> chunk;
long handle = -1;
Entry(Handle<Entry<?>> recyclerHandle) {
this.recyclerHandle = recyclerHandle;
}
void recycle() {
chunk = null;
handle = -1;
recyclerHandle.recycle(this);
}
}
执行流程图:
到此PooledByteBuf 源码分析完毕,下篇我们分享ChannelInitializer 源码,敬请期待!