PooledByteBuf 源码分析(三十五)

今天进行 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 源码,敬请期待!

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

寅灯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值