Netty内存池管理

Netty内存池管理

PooledByteBufAllocatorNetty提供的内存池实现。它用于分配和管理ByteBuf对象,减少频繁的内存分配和释放操作,提高性能。内存池将内存分为不同大小的块,每个大小对应一个内存池。内存池中的块被分为多个层级,包括ChunkPageSubpage

ChunkNetty向操作系统申请内存的单位,所有的内存分配操作也是基于Chunk完成的,Chunk可以理解为Page的集合,每个Chunk默认大小为16MPageChunk用于管理内存的单位,Netty中的Page的大小为8K,不要与Linux中的内存页Page混淆了。假如我们需要分配64K的内存,需要在Chunk中选取8个Page进行分配。

Subpage负责Page内的内存分配,假如我们分配的内存大小远小于Page,直接分配一个Page会造成严重的内存浪费,所以需要将Page划分为多个相同的子块进行分配,这里的子块就相当于Subpage。按照TinySmall两种内存规格,SubPage的大小也会分为两种情况。在Tiny场景下,最小的划分单位为16B,按16B依次递增,16B32B48B …… 496B;在Small场景下,总共可以划分为512B1024B2048B4096B四种情况。Subpage没有固定的大小,需要根据用户分配的缓冲区大小决定,例如分配1K的内存时,Netty会把一个Page等分为8个1KSubpage

PoolArena

PoolArenaPooledByteBufAllocator内部用于实际管理内存块的组件。PoolArena的数据结构包含两个PoolSubpage数组和六个PoolChunkList,两个PoolSubpage数组分别存放TinySmall类型的内存块,六个PoolChunkList 分别存储不同利用率的Chunk,构成一个双向循环链表。其中PoolSubpage用于分配小于8K的内存,PoolChunkList用于分配大于8K的内存。

PoolSubpage也是按照TinySmall两种内存规格,设计了tinySubpagePoolssmallSubpagePools两个数组,在Tiny场景下,内存单位最小为16B,按16B依次递增,共32种情况,Small场景下共分为512B1024B2048B4096B四种情况,分别对应两个数组的长度大小,每种粒度的内存单位都由一个PoolSubpage进行管理。假如我们分配20B大小的内存空间,也会向上取整找到32BPoolSubpage节点进行分配。

PoolChunkList用于Chunk场景下的内存分配,PoolArena中初始化了六个PoolChunkList,分别为qInitq000q025q050q075q100,它们分别代表不同的内存使用率。

  • qInit,内存使用率为0 ~ 25%Chunk
  • q000,内存使用率为1 ~ 50%Chunk
  • q025,内存使用率为25% ~ 75%Chunk
  • q050,内存使用率为50% ~ 100%Chunk
  • q075,内存使用率为75% ~ 100%Chunk
  • q100,内存使用率为100%Chunk

六种类型的PoolChunkList除了qInit,它们之间形成了双向链表。qInit用于存储初始分配的PoolChunk,因为在第一次内存分配时,PoolChunkList中并没有可用的PoolChunk,所以需要新创建一个PoolChunk 并添加到qInit列表中。qInit中的PoolChunk即使内存被完全释放也不会被回收,避免PoolChunk的重复初始化工作。其余类型的PoolChunkList中的PoolChunk当内存被完全释放后,PoolChunk从链表中移除,对应分配的内存也会被回收。

在使用PoolChunkList分配内存时,也就是分配大于8K的内存,其链表的访问顺序是q050->q025->q000->qInit->q075,遍历检查PoolChunkList中是否有PoolChunk可以用于内存分配。

private void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
    if (q050.allocate(buf, reqCapacity, normCapacity) || q025.allocate(buf, reqCapacity, normCapacity) ||
        q000.allocate(buf, reqCapacity, normCapacity) || qInit.allocate(buf, reqCapacity, normCapacity) ||
        q075.allocate(buf, reqCapacity, normCapacity)) {
        return;
    }

    PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
    boolean success = c.allocate(buf, reqCapacity, normCapacity);
    assert success;
    qInit.add(c);
}

这是一个折中的选择,在频繁分配内存的场景下,如果从q000开始,会有大部分的PoolChunk面临频繁的创建和销毁,造成内存分配的性能降低。如果从q050开始,会使PoolChunk的使用率范围保持在中间水平,降低了PoolChunk被回收的概率,从而兼顾了性能。

PoolChunkList

PoolChunkList负责管理多个PoolChunk的生命周期,同一个PoolChunkList中存放内存使用率相近的PoolChunk,这些PoolChunk同样以双向链表的形式连接在一起。因为PoolChunk经常要从PoolChunkList中删除,并且需要在不同的PoolChunkList中移动,所以双向链表是管理PoolChunk时间复杂度较低的数据结构。

每个PoolChunkList都有内存使用率的上下限,minUsagemaxUsage,当PoolChunk进行内存分配后,如果使用率超过maxUsage,那么PoolChunk会从当前PoolChunkList移除,并移动到下一个PoolChunkList。同理,PoolChunk中的内存发生释放后,如果使用率小于minUsage,那么PoolChunk会从当前PoolChunkList移除,并移动到前一个PoolChunkList

PoolChunk

Netty内存的分配和回收都是基于PoolChunk完成的,PoolChunk是真正存储内存数据的地方,每个PoolChunk的默认大小为16M

final class PoolChunk<T> implements PoolChunkMetric {
    final PoolArena<T> arena;
    final T memory; // 存储的数据
    private final byte[] memoryMap; // 满二叉树中的节点是否被分配,数组大小为 4096
    private final byte[] depthMap; // 满二叉树中的节点高度,数组大小为 4096
    private final PoolSubpage<T>[] subpages; // PoolChunk 中管理的 2048 个 8K 内存块
    private int freeBytes; // 剩余的内存大小
    PoolChunkList<T> parent;
    PoolChunk<T> prev;
    PoolChunk<T> next;
    // 省略其他代码
}

PoolChunk可以理解为Page的集合,Page只是一种抽象的概念,实际在NettyPage所指的是PoolChunk所管理的子内存块,每个子内存块采用PoolSubpage表示。Netty会使用伙伴算法将PoolChunk分配成2048个Page,最终形成一颗满二叉树,二叉树中所有子节点的内存都属于其父节点管理。

PoolSubpage

它的主要作用是管理内存池中的小内存块,在分配的内存大小小于一个8K时,会使用PoolSubpage进行管理。

final class PoolSubpage<T> implements PoolSubpageMetric {
    final PoolChunk<T> chunk;
    private final int memoryMapIdx; // 对应满二叉树节点的下标
    private final int runOffset; // PoolSubpage 在 PoolChunk 中 memory 的偏移量
    private final long[] bitmap; // 记录每个小内存块的状态
    // 与 PoolArena 中 tinySubpagePools 或 smallSubpagePools 中元素连接成双向链表
    PoolSubpage<T> prev;
    PoolSubpage<T> next;
    int elemSize; // 每个小内存块的大小
    private int maxNumElems; // 最多可以存放多少小内存块:8K/elemSize
    private int numAvail; // 可用于分配的内存块个数
    // 省略其他代码
}

PoolSubpage通过管理小于PageSize的内存块来优化内存分配。它从PoolChunk中预留一块连续内存区域,并将其划分为多个小块。每个PoolSubpage内部维护一个空闲块链表。当需要分配内存时,PoolSubpage会检查其内存区域中的空闲块,并从中选择一个可用的块进行分配。这样通过细化管理和复用小块内存,PoolSubpage能够高效地处理小块内存的分配请求,减少内存碎片化并提高性能。

PoolThreadCache

PoolThreadCache是本地线程缓存,内存分配时,首先尝试从线程本地缓存PoolThreadCache中获取内存块,如果缓存中没有,才会从全局内存池中获取。每个线程有自己的PoolThreadCache,可以快速分配和回收内存块,减少对全局内存池的争用。PoolThreadCache缓存TinySmallNormal三种类型的数据,而且根据堆内和堆外内存的类型进行了区分:

final class PoolThreadCache {
    final PoolArena<byte[]> heapArena;
    final PoolArena<ByteBuffer> directArena;
    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;
    // 省略其他代码
}

PoolThreadCache中有一个重要的数据结构,MemoryRegionCacheMemoryRegionCache有三个重要的属性,分别为queuesizeClasssizeMemoryRegionCache实际就是一个队列,当内存释放时,将内存块加入队列当中,下次再分配同样规格的内存时,直接从队列中取出空闲的内存块。PoolThreadCache将不同规格大小的内存都使用单独的MemoryRegionCache维护。

内存分配流程

Netty的内存分配流程首先从PoolThreadCache中尝试分配内存。对于小于8K的内存请求,Netty首先检查本地线程缓存。如果缓存中没有足够的内存块,则会向PoolArena请求。对于大于8K的内存请求,Netty直接向PoolArena请求内存,不经过本地线程缓存。PoolArena使用PoolChunk来管理较大的内存块,通过伙伴算法和二叉树结构进行内存分配。对于小内存请求,PoolArena使用PoolSubpage来管理,按位图记录内存块的使用情况。释放的内存块会被缓存到PoolThreadCache中,定期进行整理,并在线程退出时释放。

具体来说:

  1. 分配请求处理:当应用程序需要分配内存时,Netty首先检查是否可以从 PoolThreadCache中获得所需的内存块。PoolThreadCacheNetty为每个线程维护的缓存,它用于存储和管理小块内存。这种缓存机制目的是提高内存分配的速度,减少线程间的竞争。
  2. 线程缓存检查:如果 PoolThreadCache 中有足够的空闲内存块,Netty将直接从缓存中分配内存。这种方式比从全局内存池中分配内存要快,因为它避免了对全局内存池的访问和管理开销。线程缓存的使用可以显著减少内存分配和释放的延迟。
  3. 请求PoolArena:当线程缓存中的内存不足以满足请求时,Netty将向PoolArena请求内存PoolArena 负责管理整个内存池中的内存块,它会根据内存块的大小选择适当的管理策略。
  4. PoolArena内存分配:PoolArena 处理内存分配时,会根据内存块的大小选择不同的管理策略。对于较小的内存块,PoolArena 使用 PoolSubpage 进行内存分配。PoolSubpage 使用位图来跟踪和管理内存块的使用状态,来减少内存碎片。对于较大的内存块,PoolArena 使用 PoolChunkPoolChunk通过伙伴算法或其他内存管理策略来处理大块内存。PoolChunk 负责分配较大的内存区域,能够应对不同的内存请求。
  5. 内存分配和回收:内存块从PoolArenaPoolSubpagePoolChunk中分配后,会被返回给应用程序使用。释放内存时,内存块将被归还到相应的内存管理区域。根据内存块的大小和当前的缓存状态,内存块可能会被归还到线程缓存中,或者直接归还到 PoolArena 中进行进一步管理。
  6. 线程缓存整理:为了保持线程缓存的有效性,Netty 定期对 PoolThreadCache 中的内存块进行整理。这一过程可以减少内存碎片,提高内存使用效率。当线程结束时,与该线程相关的线程缓存内存也会被释放和整理。
  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

_whitepure

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

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

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

打赏作者

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

抵扣说明:

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

余额充值