让我来告诉你Netty是如何分配内存的

        创作内容丰富的干货文章很费心力,感谢点过此文章的读者,点一个关注鼓励一下作者,激励他分享更多的精彩好文,谢谢大家!


        在上一篇文章我们介绍了Netty中的ByteBuf。ByteBuf是数据的存储介质,那么Netty是如何根据需要进行内存分配的呢?这是本篇文章要介绍的内容。

内存分配的本质

        我们通过代码新创建的对象,本质上是以字节为单位存储在内存中,根据定义的类型在内存中申请相应大小的内存空间,将数据以字节为单位存储在内存中。但是这只是开辟了一个内存空间,具体要如何使用呢?操作系统会给新开辟的内存空间生成一个内存地址,暴露给程序进行调用。同时会给开辟的内存空间一个标识,标识该内存空间不可再分配了,只有在内存被回收之后,才会把标识修改为可以分配。

        内存分配和回收是需要消耗系统性能的,所以在高并发场景下,不应该进行频繁的内存分配和回收操作。Netty4使用了内存池来管理内存的分配和回收,Netty内存池参考了Slab分配和Buddy分配思想。Slab分配是将内存分割成大小不等的内存块,在用户线程请求时根据请求的内存大小分配最为贴近Size的内存快,减少内存碎片同时避免了内存浪费。Buddy分配是把一块内存块等量分割回收时候进行合并,尽可能保证系统中有足够大的连续内存。

        Netty 总体的分配策略如下。

  • 为了避免线程间锁的竞争和同步,每个I/O线程都对应一个 PoolThreadCache,负责当前线程使用非大内存的快速申请和释放。

  • 当从 PoolThreadCache 中获取不到内存时,就从 PoolArena 的内存池中分配。当内存使用完并释放时,会将其放到 PoolThreadCache 中,方便下次使用。若从 PoolArena的内存池中分配不到内存,则从堆内外内存中申请,申请到的内存叫 PoolChunk。当内存块的大小默认为16MB时,会被放入 PoolArea 的内存池中,以便重复利用。当申请大内存时(超过了 PoolChunk 的默认内存大小16MB),直接在堆外或堆内内存中创建(不归PoolArea管理),用完后直接回收。

        Netty内存管理层级结构如下图所示,其中右边是内存管理的3个层级,分别是本地线程缓存、分配区arena、系统内存。左边是内存块区域,不同大小的内存块对应不同的分配区。

        Netty内存分配的流程如下所示:

  1. Netty在具体分配内存之前,会先获取本次内存分配的大小。具体的内存分配由PoolArena统一管理,先从线程本地缓存PoolThreadCache中获取,线程本地缓存采用固定长度队列缓存此线程之前用过的内存。

  2. 若本地线程无缓存,则判断本次需要分配的内存大小,若小于512B,则先从PoolArena的tinySubpagePools缓存中获取;若大于或等于512B且小于8KB,则先从smallSubpagePools缓存中获取,上述两种情况缓存的对象都是PoolChunk分配的PoolSubpage。若大于或等于8KB或在SubpagePools缓存中分配失败,则从PoolChunkList中查找可分配的PoolChunk。

  3. 若PoolChunkList分配失败,则创建新的PoolChunk,由PoolChunk完成具体的分配工作,最终分配成功后,加入对应的PoolChunkList中。若分配的是小于8KB的内存,则需要把从PoolChunk中分配的PoolSubpage加入PoolArena的SubpagePools中。         

        Netty最新版本基于jemalloc4算法进行内存分配,去掉了Tiny,保留了small、normal、Huge。

  • small :[0-28k]

  • normal:[28k-16M]

  • huge: > 16M

        基于jemalloc4的Netty内存结构如下图:

        

        Netty中和内存分配相关的类有PoolArena、ChunkList、Chunk、Page、SubPage。

        PoolArena用来管理池化的内存结构,里面有一个 PoolSubpage 类型的数组,用来存储零散内存,和多个按照使用率划分的多个PoolChunkList 类型的对象,Netty把多个Chunk按照使用率存储到对应的PoolChunkList中,PoolChunkList是一个双向链表,把各种使用率的PoolChunkList联系起来。

private void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int sizeIdx, PoolThreadCache threadCache) {
        assert lock.isHeldByCurrentThread();
        if (q050.allocate(buf, reqCapacity, sizeIdx, threadCache) ||
            q025.allocate(buf, reqCapacity, sizeIdx, threadCache) ||
            q000.allocate(buf, reqCapacity, sizeIdx, threadCache) ||
            qInit.allocate(buf, reqCapacity, sizeIdx, threadCache) ||
            q075.allocate(buf, reqCapacity, sizeIdx, threadCache)) {
            return;
        }

        // Add a new chunk.
        PoolChunk<T> c = newChunk(sizeClass.pageSize, sizeClass.nPSizes, sizeClass.pageShifts, sizeClass.chunkSize);
        boolean success = c.allocate(buf, reqCapacity, sizeIdx, threadCache);
        assert success;
        qInit.add(c);
    }

      分配内存时为什么选择从 q050 开始

        1、qInit 的 Chunk 利用率低,但不会被回收。

        2、q075 和 q100 由于内存利用率太高,导致内存分配的成功率大大降低,因此放到最后。

         3、q050 保存的是内存利用率 50%100% 的 Chunk,这应该是个折中的选择。这样能保证 Chunk 的利用率都会保持在一个较高水平提高整个应用的内存利用率,并且内存利用率在 50%100% 的 Chunk 内存分配的成功率有保障。

        4、当应用在实际运行过程中碰到访问高峰,这时需要分配的内存是平时的好几倍需要创建好几倍的 Chunk,如果先从 q000 开始,这些在高峰期创建的 Chunk 被回收的概率会大大降低,延缓了内存的回收进度,造成内存使用的浪费。  

PoolChunkList

        PoolChunkList 负责将 PoolChunk 添加或移除出 PoolChunk 形成的双向链表。如果用户使用池化思想分配内存,Netty 直接会把数据存入到提前分配好的 PoolChunk 中,PoolChunk 中的可用内存容量是一直动态变化的,如果 PoolChunk 链表非常长,偏偏可以分配的 PoolChunk 在链表的最后,那么程序如何快速定位到合适的 PoolChunk 呢? Netty 的实现思路是在 PoolChunkList 中定义了 minUsage 和 maxUsage 两个成员变量,在 PoolChunkList 中按照最小使用率和最大使用率区间,来分组管理 PoolChunk 。如果每一个PoolChunk 内存块大小是16MB,现在需要6MB内存来存储数据,那么程序直接从内存利用率为0 - 50%的 PoolChunkList 链表中寻找即可,因为这个链表中每一个 PoolChunk 对象内存使用率最大只有50%,也就是说最少都还有8MB内存可以被分配,肯定可以满足6MB数据的内存存储需求。PoolChunkList 也是一个双向链表结构,它是按照最小使用率和最大使用率内存区间排序形成的,这样做的好处是让由于 PoolChunk 对象的可用内存发生改变时,可以快速移动到自己所属的最新的 PoolChunkList 对象上,方便接下来的程序内存分配。

PoolChunk

        可以把Chunk看作一块大的内存,这块内存被分成了很多小块的内存,Netty在使用内存时,会用到其中一块或多块小内存。通过0和1来标识每个内存块的占用情况,通过内存的偏移量和请求内存空间大小reqCapacity来决定读/写指针。内存池在分配内存时,只会预先准备固定大小和数量的内存块,不会请求多少内存就恰好给多少内存,因此会有一定的内存被浪费。使用完后交还给PoolChunk并还原,以便重复使用。

        PoolChunk内部维护了一棵平衡二叉树,默认由2048个page组成,一个page默认为8KB,整个Chunk默认为16MB,其二叉树结构如图所示。

        

        在Netty的最新版本中,把page由run来管理,并存储在runsAvailMap这个结构中。每一个run的首个runOffset和最后runOffset存储在runsAvailMap中。runsAvail是一个整形优先级队列数组,存储的是handle。handle的格式如下图所示:

oooooooo ooooooos ssssssss ssssssue bbbbbbbb bbbbbbbb bbbbbbbb bbbbbbbb
o: chunk中的page偏移量, 15bit
s: run中的page数量, 15bit
u: isUsed 此区域是否被使用, 1bit
e: isSubpage 是否用于small规格的分配, 1bit
b: 给small规格使用的,poolSubPage内分配的内存都是同一容量大小,不能定义不同的大小。
   如果poolSubPage分配到8192B,每一个内存块32B,那么总共可以分配256个,这256个可以
   用一个bit数组来表示,bitmapIdx其实就是这个bit数组的索引下标, 如果不是small规格,
   32位都是0。32bit
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值