netty源码阅读二(内存池之PoolChunk)

这个分享阅读的是4.1.52.Final-SNAPSHOT这个版本的源码

jemalloc论文

netty源码阅读一(内存池之SizeClasses)

netty源码阅读二(内存池之PoolChunk)

netty源码阅读三(内存池之PoolSubpage)

netty源码阅读四(内存池之PoolChunkList)

netty源码阅读五(内存池之PoolArena和PoolChunkList)

poolChunk表示的内存池中一整块的内存,也是内存池向java虚拟机申请和释放的最小单位,即内存池每次会向虚拟机申请一个PoolChunk内存来进行分配,并在PoolChunk空闲时将PoolChunk中的内存释放。

内存池对于内存的分配其最终分配的是内存所处的PoolChunk以及PoolChunk下的句柄handle。

重要术语

在介绍PoolChunk代码之前先介绍几个比较重要的术语。

  • page:page是PoolChunk的分配的最小单位,默认的1个page的大小为1kB
  • run:表示的是一个page的集合
  • handle:句柄,用于表示poolChunk中一块内存的位置,大小,使用情况等信息,下面的图展示了一个handle的对应的位数的含义。可以看到一个handle其实是一个64为的数据,其前15位表示的是这个句柄所处的位置,即第几页,然后15位表示的是这个句柄表示的是多少页,isUsed表示这一段内存是否被使用,isSubpage表示的这一段内存是否用于subPage的分配,bitmapIdx表示的是这块内存在subPage中bitMap的第几个

源码阅读

PoolChunk的数据结构

下面是PoolChunk主要的数据结构。

final class PoolChunk<T> implements PoolChunkMetric {
	...
	//所处的PoolArena
    final PoolArena<T> arena;
    //维护的内存块,用泛型区分是堆内存还是直接内存
    final T memory;
    //表示这个PoolChunk是没进行池化操作的,主要为Huge的size的内存分配
    final boolean unpooled;
    final int offset;
    //存储的是有用的run中的第一个和最后一个Page的句柄
    private final IntObjectMap<Long> runsAvailMap;
    //管理所有有用的run,这个数组的索引是SizeClasses中page2PageIdx计算出来的idx
    //即sizeClass中每个size一个优先队列进行存储
    private final PriorityQueue<Long>[] runsAvail;
    //管理这个poolchunk中所有的poolSubPage
    private final PoolSubpage<T>[] subpages;
    //一个page的大小
    private final int pageSize;
    //pageSize需要左移多少位
    private final int pageShifts;
    //这个chunk的大小
    private final int chunkSize;
    //主要是对PooledByteBuf中频繁创建的ByteBuffer进行缓存,以避免由于频繁创建的对象导致频繁的GC	
    private final Deque<ByteBuffer> cachedNioBuffers;
    //空闲的byte值
    int freeBytes;
    //所处的PoolChunkList
    PoolChunkList<T> parent;
    //所处双向链表前一个PoolChunk
    PoolChunk<T> prev;
    //所处双向两边的后一个PoolChunk
    PoolChunk<T> next;
}

 上面的数据结构中主要是runsAvailMap,runsAvail,subPagesmemory这4块数据,下面图描述了这几块数据具体存储的内容。

其中memory部分绿色表示已经分配了的页,空白的表示还没被分配的页,青色部分表示的被分配为subPage的页。

可以看到runsAvailMap存储的是runOffset->handle之间的键值对,并且其存储的是空闲块的第一页和最后一页的句柄。runsAvail则是维护的一个优先队列数组,其数组的索引其实是size对应的sizeIdx,可以看到空闲页为2页的内存的三块内存的句柄都存在了一个优先队列中,而空闲页为5页的内存则存在另一个对应位置的优先队列中。subPages则数一个PoolSubPage数组,其数组的索引为page的runOffset,存储的是以这个offset开始的PoolSubPage对象

PoolChunk主要的是allocate和free这两个方法,处理分配和释放两个操作,下面来介绍一下这两个方法。

allocate

可以看到这个allocate方法其实就是将subPage的分配委托给allocateSubpage方法,对于不是subPage的分配委托给了allocateRun进行操作。

boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int sizeIdx, PoolThreadCache cache) {
    final long handle;
    //这里的sizeIdx表示的是sizeClass的size2sizeIdx计算的对应的sizeIdx
    //这里的smallMaxSizeIdx表示的是最大的subPage的对应的索引
    if (sizeIdx <= arena.smallMaxSizeIdx) {
        // 这种size则以subPage的形式进行分配
        handle = allocateSubpage(sizeIdx);
        if (handle < 0) {
            return false;
        }
        assert isSubpage(handle);
    } else {
    	//利用allocateRun分配多个整的page
        int runSize = arena.sizeIdx2size(sizeIdx);
        handle = allocateRun(runSize);
        if (handle < 0) {
            return false;
        }
    }
    //从cachedNioBuffers获取缓存的ByteBuffer,一个PoolChunk下的所有的这些ByteBuffer 
    //其实指向的都是同一块区域,即memory
    ByteBuffer nioBuffer = cachedNioBuffers != null? cachedNioBuffers.pollLast() : null;
    //初始化
    initBuf(buf, nioBuffer, handle, reqCapacity, cache);
    return true;
}

allocateRun

allocateRun是分配多个page操作,其主要操作是从runAvail中找到最接近当前需要分配的size的内存块,然后将其进行切分出需要分配的内存块,并将剩下的空闲块再存到runAvail和runsAvailMap中。

private long allocateRun(int runSize) {
    int pages = runSize >> pageShifts;
    //根据page的数量计算对应的pageIdx
    //因为runAvail这个数组用的则是这个idx为索引的
    int pageIdx = arena.pages2pageIdx(pages);
    //这里的runsAvail保证的是runsAvail和runsAvailMap数据的同步
    synchronized (runsAvail) {
        //从当前的pageIdx从ranAvail中找到最近的能进行此次分配idx索引
        int queueIdx = runFirstBestFit(pageIdx);
        if (queueIdx == -1) {
            return -1;
        }
        //从这个索引中获取对应的run的数据,即为能进行此次分配的空闲段
        PriorityQueue<Long> queue = runsAvail[queueIdx];
        long handle = queue.poll();
        assert !isUsed(handle);
        //从queue和runsAvailMap中移除这个handle
        removeAvailRun(queue, handle);
        if (handle != -1) {
        	//将这一块内存进行切分,剩余空闲的内存继续存储到ranAvail和runsAvailMap中
            handle = splitLargeRun(handle, pages);
        }
        //更新freeBytes
        freeBytes -= runSize(pageShifts, handle);
        return handle;
    }
}

下面图描述了对上面那幅图进行了一次3个page的分配操作后对应的内存的数据结构,其中红色表示的是这次分配的内存,可以看到原来在5kB的内存数据到2kB中,并且存储在runsAvaliMap中的对应的key也由原来的12移到了现在的15

 allocateSubpage

这个方法是分配一个subPage,其主要的逻辑是先对sizeIdx这个索引在sizeClasses中所对应的size为基础的elemSize获取一个这个elemSize和pageSize的最小公倍数大小的内存,将这块内存分为大小相等的以elmSize的subPage利用PoolSubPage来进行维护。

private long allocateSubpage(int sizeIdx) {
    //根据sizeIdx计算出其对应的PoolSubpage,arena以sizeIdx为key存储了一个散列表
    //来存储PoolSubpage,其每个链表的头都是一个特殊的不做内存分配的PoolSubpage的head
    //对于其对应链表的操作都需要对这个链表的head加锁
    PoolSubpage<T> head = arena.findSubpagePoolHead(sizeIdx);
    synchronized (head) {
        //计算第一个对这个sizeIdx对应的size与pageSize的最小公倍数
        int runSize = calculateRunSize(sizeIdx);
        //获取这个runSize的对应打下的内存
        long runHandle = allocateRun(runSize);
        if (runHandle < 0) {
            return -1;
        }
        int runOffset = runOffset(runHandle);
        int elemSize = arena.sizeIdx2size(sizeIdx);
        //把这个分配的runSize切分为runSize/elemSize个相同的elemSize大小的subPage
        //利用PoolSubpage进行分配
        PoolSubpage<T> subpage = new PoolSubpage<T>(head, this, pageShifts, runOffset,
                           runSize(pageShifts, runHandle), elemSize);
        //将这个subPage存在subpages中                   
        subpages[runOffset] = subpage;
        return subpage.allocate();
    }
}

free

free的进行释放操作,主要操作如果是subpage,利用PoolSubpage进行释放。对于多页的释放则会利用runsAvailMap合并其前后的空闲的内存块,因为runsAvailMap中存储了空闲内存块的头和尾,所以对内存块的合并很简单,即为以当前的头和尾的前一个或者后一个为key能否找到对应的空闲内存合并即可。

void free(long handle, int normCapacity, ByteBuffer nioBuffer) {
	//释放的是subPage
    if (isSubpage(handle)) {
        int sizeIdx = arena.size2SizeIdx(normCapacity);
        PoolSubpage<T> head = arena.findSubpagePoolHead(sizeIdx);
        PoolSubpage<T> subpage = subpages[runOffset(handle)];
        assert subpage != null && subpage.doNotDestroy;
        synchronized (head) {
        	//PoolSubPage释放这块内存,返回true则表示这块PoolSubPage还在用
            if (subpage.free(head, bitmapIdx(handle))) {
                //the subpage is still used, do not free it
                return;
            }
        }
    }
    //start free run
    int pages = runPages(handle);
    synchronized (runsAvail) {
        //与这块内存前后相邻的内存空闲内存进行合并
        long finalRun = collapseRuns(handle);
        //将IS_USED和IS_SUBPAGE标志位设置为0
        finalRun &= ~(1L << IS_USED_SHIFT);
        //if it is a subpage, set it to run
        finalRun &= ~(1L << IS_SUBPAGE_SHIFT);
        //将合并后的句柄存储到runAvail和runsAvailMap中
        insertAvailRun(runOffset(finalRun), runPages(finalRun), finalRun);
        freeBytes += pages << pageShifts;
    }
    //将这个ByteBuf创建的ByteBuffer存到cachedNioBuffers缓存中
    if (nioBuffer != null && cachedNioBuffers != null &&
        cachedNioBuffers.size() < PooledByteBufAllocator.DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK) {
        cachedNioBuffers.offer(nioBuffer);
    }
}

下面图则是对开始的内存数据将offset为8的内存块释放后其内存数据结构的情况,可以看到其将前面和后面空闲的内存块合并了,成为了5页的内存块,其对应的runsAvailMap和runsAvail中的数据也进行了相应的改变。 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值