Netty源码解析(六)之PooledByteBufAllocator

PooledByteBufAllocator是Netty中比较复杂的一种ByteBufAllocator,因为他涉及到对内存的缓存、分配和释放策略。PooledByteBufAllocator,顾名思义,是对内存做了池化,也就是缓存一定容量的内存,每次用PooledByteBufAllocator申请ByteBuf时,不需要重新向操作系统或者JVM申请内存,而是可以直接从预先申请好的内存池中取一块内存(类似于线程池),因此在需要频繁申请和释放内存的场景下,PooledByteBufAllocator比UnpooledByteBufAllocator性能明显更好。

一. PooledByteBufAllocator概述

为了保证线程安全以及减少不同线程在申请ByteBuf时的竞争,PooledByteBufAllocator为每个不同的线程都缓存了一些内存。

PooledByteBufAllocator分配ByteBuf主要分为两个步骤:

  1. 拿到线程局部缓存PoolThreadCache,PoolThreadCache保存在一个ThreadLocal上,因此每个线程对应一个PoolThreadCache。
  2. 在PoolThreadCache上进行内存分配
  3. 如果该线程的PoolThreadCache上没有足够的内存可供分配,则在线程局部缓存的Arena上进行内存分配。

以分配HeapByteBuf为例(分配DirectByteBuf的逻辑是一样的),由于PooledByteBufAllocator有可能被多个线程同时使用,因此PooledByteBufAllocator内部有一个PoolThreadLocalCache和一个PoolArena数组,每一个PoolThreadCache和PoolArena中都缓存了一些内存,PoolThreadCache和线程是一一对应的,但PoolArena和线程是一对多的关系,某个线程需要分配ByteBuf时先从自己的PoolThreadCache上分配,分配不到再到对应的PoolArena中去取。

protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) {
    //拿到该线程对应的PoolThreadCache
    PoolThreadCache cache = threadCache.get();
    //拿到该PoolThreadCache对应的PoolArena
    PoolArena<byte[]> heapArena = cache.heapArena;

    final ByteBuf buf;
    if (heapArena != null) {
        //从cache或heapArena上分配一个ByteBuf
        buf = heapArena.allocate(cache, initialCapacity, maxCapacity);
    } else {
        buf = PlatformDependent.hasUnsafe() ?
                new UnpooledUnsafeHeapByteBuf(this, initialCapacity, maxCapacity) :
                new UnpooledHeapByteBuf(this, initialCapacity, maxCapacity);
    }

    return toLeakAwareBuffer(buf);
}

我们可以画出Thread、PoolThreadCache和PoolArena之间的关系 

二. PoolThreadCache的结构

PoolThreadCache中缓存的内存是以MemoryRegionCache数组的形式存在的,而一个MemoryRegionCache中又包含了一个Entry队列,每一个Entry可以代表一个特定大小的内存,一个MemoryRegionCache的所有Entry所代表的内存大小是一样的。MemoryRegionCache的结构如下:

MemoryRegionCache的SizeClass是一个枚举类型,表示他的Entry的内存的大小的范围:Tiny(0~496B),Small(512B~4K),Normal(8K~16M),而size则表示每个Entry的具体的内存大小。值得注意的是,大于32K且小于16M的内存块是不在PoolThreadCache中进行缓存的。

PoolThreadCache中有三个MemoryRegionCache数组:tinySubPageHeapCaches,smallSubPageHeapCaches和normalSubPageHeapCaches,分别对应三种不同的SizeClass(directCaches的原理与heapCaches是一样的,这里就只以heapCaches为例)。这三个数组的结构如下:

数组中的每一个节点都是一个MemoryRegionCache,其中包含了一组特定内存大小的Entry。例如,如果向PooledByteBufAllocator申请一个20B大小的HeapByteBuf,PooledByteBufAllocator会从当前线程的PoolThreadCache的tinySubPageHeapCaches数组中找到size为32B的那个MemoryRegionCache(PooledByteBufAllocator会将20B规整化为32B),再从他的queue中取一个Entry返回给申请者。

 三、PoolArena的结构

PooledByteBufAllocator中有三个重要的数据结构tinySubpagePools,smallSubpagePools和chunkList。

1. ChunkList和Chunk

ChunkList

PoolArena每次向操作系统申请直接内存或向JVM申请堆内存时,都是以Chunk为单位申请,Chunk的默认大小为16M,一个Chunk又包含2K个Page,每个Page大小为16M/2K=8K,一个ChunkList包含一个Chunk双向列表,PoolArena中又有5个ChunkList,且这5个ChunkList又组成了一个双向列表,这5个ChunkList分别为qinit,q000,q025,q050,q075,q100。

这5个ChunkList有什么区别呢?每个ChunkList的名字后面的数字表示这个ChunkList所维护的Chunk的使用率,qinit维护的Chunk的使用率为0~25%,q000为1%~50%,q025为25%~75%,q050为50%~100%,q075为75%~100%,q100为100%。

例如,如果一个Chunk是新申请的,那么他的使用率为0,他会在qinit的ChunkList中,之后某个线程占用了这个Chunk的100个Page,他的使用率变成800K/16M≈5%,那么他就会被移到q000中。

为什么这5个ChunkList的使用率范围会有重叠呢?这是为了防止Chunk的使用率频繁变化而导致他频繁的切换ChunkList。例如,q025中的一个Chunk,当他的使用率从60%变为30%,他不会切换到q000中,而是继续在q025中。

Chunk

Chunk中用一个平衡二叉树来表示他的内存的使用状况。树中的一个节点代表某一范围的内存,同一深度的节点所代表的内存的大小是相同的。并且用一个byte数组memoryMap表示所有节点的状态,节点的状态反映了他所代表的这段内存的使用情况,节点的状态有三种情况:

  1. 如果这段内存全部被使用,节点的状态值为12(最大深度+1)
  2. 如果这段内存部分被使用,节点的状态值为他所在的深度+1
  3. 如果这段内存完全未被使用,节点的状态值为他所在的深度

每次申请内存时,都会根据申请的大小从对应的深度开始查找,例如,如果申请一个4M的内存,就会从d=2开始查找(因为深度为2的Node都是大小为4M的内存):

  1. 先从Node4开始,如果Node4的状态值为2,那么就表示0~4M这一块内存完全未被使用,就会将0~4M这一块内存返回给申请者,并修改相应的节点的状态:Node4的状态值改为12(表示0~4M这一块内存已经全部被使用),Node2的状态值改为2(Node2所在的深度+1),Node1的状态值改为1(因为0~8M和0~16M只是部分被使用)
  2.  如果Node4的状态值为3或者12,表示0~4M这一块内存已经部分或完全被使用了,则继续查看Node5的状态
  3. 如果d=2的节点的状态值都不为2,表示这个Chunk已经不存在连续的4M大小的内存了,则申请失败,继续从ChunkList的下一个Chunk中去申请。

2. tinySubpagePools和smallSubpagePools

Chunk分配内存时是以Page(size为8K)为单位,很多时候这个size还是太大了,如果申请一个1K的内存,Chunk返回一个8K的Page,这样属实是有点浪费了,因此PoolArena中还有两个类型为PoolSubpage的数组: tinySubpagePools和smallSubpagePools。

tinySubpagePools和smallSubpagePools的结构与上面讲解PoolThreadCache中所提到的tinySubPageCaches,smallSubPageCaches的结构类似,所不同的是数组的类型一个是PoolSubpage,一个是MemoryRegionCache,且tinySubpagePools和smallSubpagePools中相同大小的PoolSubpage是以双向链表的形式连接在一起,而不是队列,每个双向链表的head节点都是一个虚拟节点,如图所示:

 每个PoolSubpage都是由某个Chunk的某个Page转换而来的,例如,如果向PoolArena申请一个32B大小的内存,那么PoolArena会从ChunkList中选一个Chunk,然后将这个Chunk的某个Page转换为PoolSubpage,再将这个PoolSubpage加入到tinySubPagePools数组中。每个Chunk都有一个长度为2048的数组,用来表明某个Page是否转换为了PoolSubpage:

PoolSubpage[] subpages

例如,如果subgage[2]不为null,表明第2个Page(对应二叉树的Node2050)已经转换为了PoolSubpage,即16~24K这一段内存是按subpage分配的;如果subpage[0]==null,表明第0个Page(对应二叉树的Node2048)仍然是一个正常的Page。

PoolSubPage中有几个重要的属性:

 我们可以画出一个完整的tinySubpagePools数组、Chunk、Page、Subpage之间的关系,如图所示:

四. ByteBuf的回收

由于一开始每个线程的PoolThreadCache中都是没有缓存任何内存的,所以一开始申请内存时必然是会从PoolArena中去申请,PoolArena会根据情况选择向操作系统(直接内存)或JVM(堆内存)申请新的内存来new一个Chunk或者从ChunkList中选一个已有的Chunk来进行分配,这一点我们前面已经分析过了。

当某个线程调用release()方法去释放他所持有的一个ByteBuf时,通常不会直接释放给PoolArena,而是将这块内存放入自己的PoolThreadCache中,这样,下次该线程再申请一个相同大小的内存时,就会在他的PoolThreadCache中的tinySubPageCaches或smallSubPageCaches或normalSubPageCaches数组中找到之前被释放的那块内存重新使用。而对于PoolArena来说,这块内存是一直被该线程占用着的,处于已被使用的状态。只有在以下几种情况才会将内存归还给PoolArena:

  1. 线程已经结束,那么该线程的PoolThreadCache会将所有他缓存的内存归还给PoolArena,这样其他线程就可以在该PoolArena上申请这些内存了。
  2. 释放的内存块大于32K,PoolThreadCache最大只缓存32K的内存块,因此如果release一个大于32K的内存,会直接归还给PoolArena。
  3. 相应的MemoryRegionCache的队列长度达到最大值,再release对应大小的内存块时,不会加入到MemoryRegionCache的队列上去,而是直接归还给PoolArena。

(完)

猜你喜欢

转载自blog.csdn.net/benjam1n77/article/details/123356426
今日推荐