10、Netty内存池之PoolChunk与PoolSubPage

一、原理

请移步到《netty的内存池设计》

二、PoolChunk与PoolSubPage

2.1 PoolChunk

2.1.1 简介

PoolChunk表示一个内存块,默认16M,用于管理分配内存,以下为PoolChunk的类图

  • PoolChunkMetric:接口,usage方法用于获取PoolChunk的使用率;chunkSize方法返回PoolChunk的大小;freeBytes方法返回剩余可用内存

2.1.2 字段说明

java 复制代码
// 值为 31,用于计算log2N的值指数值
private static final int INTEGER_SIZE_MINUS_ONE = Integer.SIZE - 1;
//内存池管理,用于根据分配内存的类型和PoolChunk的内存占用率分给内存
final PoolArena<T> arena;
//对于直接内存分配,这个字段类型为ByteBuffer
//对于堆内存分配,这个字段类型为byte[]
final T memory;
//标识是否启用池化管理内存
final boolean unpooled;
//指定memory的偏移,默认是从0开始
final int offset;
//用于记录大顶堆二叉树,元素值为层数,在某个节点被分配出去后,对一个下标的值将会被设置为12
private final byte[] memoryMap;
//和memoryMap一样用于记录大顶堆二叉树,元素值为层数
//但是它的值不会因为内存的分配而改变,这样我们可以通过下标找回原来的值
private final byte[] depthMap;
//当用户申请一个小于8k的内存时,netty会从平衡二叉树中分配一个8k的内存,然后
//封装成 PoolSubpage 存到这个数组中
private final PoolSubpage<T>[] subpages;

//这个值等于 ~(pageSize - 1) ,pageSize是一个2的n次幂的值,所有的bit位上只有一个1,这个1在第14个bit位上,减1后,低13位全部变成1,高19位全是零
//然后取反,那么高19位全是1,低13位全是0,这样任何比pageSize小的数与 subpageOverflowMask 相与都会等于零
private final int subpageOverflowMask;
//pageSize默认为8k-》8192
private final int pageSize;
//页偏移,用于计算层数,这个值为log2(pageSize),pageSize为8k,所以pageShifts为13
private final int pageShifts;
//二叉树的深度,默认是11,层数从0开始
private final int maxOrder;
//PoolChunk的大小,默认是 1 << 24 = 16M
private final int chunkSize;
//log2(chunkSize) = 24
private final int log2ChunkSize;
//表示这个16M内存最大能够分配多少个8k,很显然是 (1 << 24) / (1 << 13) = 1 << 11 = 2048个 
private final int maxSubpageAllocs;
/** Used to mark memory as unusable */
//当二叉树中一个节点被分配出去之后,这个节点的值就会被设置为 unusable = 12
private final byte unusable;

// Use as cache for ByteBuffer created from the memory. These are just duplicates and so are only a container
// around the memory itself. These are often needed for operations within the Pooled*ByteBuf and so
// may produce extra GC, which can be greatly reduced by caching the duplicates.
//
// This may be null if the PoolChunk is unpooled as pooling the ByteBuffer instances does not make any sense here.
//用于缓存ByteBuffer对象,有时用户会希望将ByteBuf转化成JDK的ByteBuffer对象
//为了节省创建ByteBuffer的开销,这里做个缓存
private final Deque<ByteBuffer> cachedNioBuffers;

//当前PoolChunk还剩余多少内存可分配
private int freeBytes;

// PoolChunkList 是一个集合类,用于集合PoolChunk
//内部使用链表实现,主要用于PoolArean,区分不同使用率的PoolChunk
//相同使用率的PoolChunk将被PoolChunkList综合在一起
PoolChunkList<T> parent;
//前一个PoolChunk,用于PoolChunkList
PoolChunk<T> prev;
//后一个PoolChunk,用于PoolChunkList
PoolChunk<T> next;

2.1.3 构造器

java 复制代码
PoolChunk(PoolArena<T> arena, T memory, int pageSize, int maxOrder, int pageShifts, int chunkSize, int offset) {
    //指定了二叉树深度,页大小,页偏移,那么一定是需要池化的
    unpooled = false;
    //指定PoolArean,内存池管理,后面会讲
    this.arena = arena;
    //如果是直接内存,这里memory的类型为ByteBuffer,如果是堆内存,那么这个memory的类型为byte[]
    this.memory = memory;
    //指定页大小
    this.pageSize = pageSize;
    //指定页偏移,用于计算层数
    this.pageShifts = pageShifts;
    //指定二叉树深度
    this.maxOrder = maxOrder;
    //指定PoolChunk内存大小
    this.chunkSize = chunkSize;
    //指定分配内存时,在memory中的偏移
    this.offset = offset;
    //二叉树某个节点被分配出去后,用12标记
    unusable = (byte) (maxOrder + 1);
    // 24
    log2ChunkSize = log2(chunkSize);
    //用于判断用户请求内存是否大于pageSize
    subpageOverflowMask = ~(pageSize - 1);
    //初始时,PoolChunk可用的剩余内存为16M
    freeBytes = chunkSize;

    assert maxOrder < 30 : "maxOrder should be < 30, but is: " + maxOrder;
    //一个16M的PoolChunk最多能分配多少个8K的内存
    maxSubpageAllocs = 1 << maxOrder;

    // Generate the memory map.
    //将二叉树映射到一个数组中,需要多长,2的12次幂
    memoryMap = new byte[maxSubpageAllocs << 1];
    depthMap = new byte[memoryMap.length];
    //从1开始
    int memoryMapIndex = 1;
    for (int d = 0; d <= maxOrder; ++ d) { // move down the tree one level at a time
        //计算当前层有多少个节点
        int depth = 1 << d;
        //给当前层上的所有节点值赋值(当前层数)
        for (int p = 0; p < depth; ++ p) {
            // in each level traverse left to right and set value to the depth of subtree
            memoryMap[memoryMapIndex] = (byte) d;
            depthMap[memoryMapIndex] = (byte) d;
            memoryMapIndex ++;
        }
    }
    //初始化PoolSubPage数组
    subpages = newSubpageArray(maxSubpageAllocs);
    //初始化ByteBuffer缓存双端队列
    cachedNioBuffers = new ArrayDeque<ByteBuffer>(8);
}

2.2 PoolSubPage

2.2.1 简介

PoolSubPage用于分配小于8k的内存,整个PoolSubPage大小为8k,内部按照tiny或者small继续划分成n份,用一个long[]类型的bitMap进行记录分配与释放


  • PoolSubpageMetric
java 复制代码
//pageSize默认是8k,没份16字节,那么这个方法返回 8k / 16 = 512
int maxNumElements();

//返回还未被分配的份数
int numAvailable();

//返回每份字节大小
int elementSize();

//返回每个页的大小,默认是8k
int pageSize();

2.2.2 字段说明

java 复制代码
//表示当前PoolSubPage分配的8k来自哪个PoolChunk
final PoolChunk<T> chunk;
//当前分配的8k对应的二叉树节点在memoryMap数组中的下标
private final int memoryMapIdx;
//在PoolChunk.memory的开始偏移位置
private final int runOffset;
//页大小,默认8k
private final int pageSize;
//用于记录内存申请与释放
private final long[] bitmap;

//前驱节点
PoolSubpage<T> prev;
//后继节点
PoolSubpage<T> next;
//是否需要被销毁
boolean doNotDestroy;
//每份大小
int elemSize;
//pageSize / elemsize,表示pageSize按elemsize能够分成多少份
private int maxNumElems;
//bitMap数组的长度
private int bitmapLength;
//每次分配掉一份内存之后,会预计算下一个可用的bit位
private int nextAvail;
//剩余可分配的份数
private int numAvail;

三、内存的分配

java 复制代码
//buf还未初始化,没有设置偏移,容量等属性
//reqCapacity为用户真实申请的内存大小
//normCapacity为最接近reqCapacity的一个2的n次幂的值
boolean io.netty.buffer.PoolChunk#allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
    final long handle;
    if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize
        //申请大于8k的内存
        handle =  allocateRun(normCapacity);
    } else {
        //申请小于8k的内存
        handle = allocateSubpage(normCapacity);
    }
    //没有申请到内存,返回分配失败
    if (handle < 0) {
        return false;
    }
    //从ByteBuffer缓存队列中获取一个
    ByteBuffer nioBuffer = cachedNioBuffers != null ? cachedNioBuffers.pollLast() : null;
    //初始化PooledByteBuf
    initBuf(buf, nioBuffer, handle, reqCapacity);
    return true;
}

以上方法首先会判断用户申请的内存是否大于8k,大于8k的从二叉树中直接分配,小于8k的除了会从二叉树中分配置之外,还会做进一步的处理,申请到内存之后对 PooledByteBuf 进行初始化

3.1 分配大于8k的内存

我们先简单回顾一下在《netty的内存池设计》

博文中提到的关于大于8k内存的申请与释放原理,首先netty会构建一个大顶堆的平衡二叉树。

每个节点表示的内存大小为父节点的一半,总共11层,分配内存时将对应节点的值设置为12,释放时将对应节点的值恢复即可,下面我们来分析下具体的代码实现。

java 复制代码
private long io.netty.buffer.PoolChunk#allocateRun(int normCapacity) {
    //计算当前需要分配的内存所在二叉树的层数
    //假设我们需要分配一个9kb的内存,那么最接近9kb的2的n次幂的值为16k
    // 11 - (14 - 13) = 10
    int d = maxOrder - (log2(normCapacity) - pageShifts);
    //通过二叉树查找能够分配对应内存的节点,返回其在memoryMap数组中的下标
    int id = allocateNode(d);
    if (id < 0) {
        return id;
    }
    //计算剩余可用内存大小
    freeBytes -= runLength(id);
    return id;
}

上面有一段公式 maxOrder - (log2(normCapacity) - pageShifts) 这个公式是用于计算某个内存大小对应二叉树中的层数,为什么这么计算呢?

首先我们知道8kb所在的层数是第11层,那么假设我要申请4M的内存,这个4M所在层数与8kb相差多少呢?是不是相差 log2(4M) - log2(8k),既然知道了差值,那么用11减去

这个差值就得到了4M所在的层数。

下面我们继续分析下netty是如何从二叉树中查找可分配的节点的

java 复制代码
private int io.netty.buffer.PoolChunk#allocateNode(int d) {
    //从1开始
    int id = 1;
    // 1 << d 表示第 d 层第一个节点在memoryMap中的下标,由于这个下标值是一个2的d次幂的值,那么其所有bit位只有第d+1位是1
    //取负值之后,低d位全部为0,d位以上全是1,等价于 ~((1 << d) - 1)
    int initial = - (1 << d); // has last d bits = 0 and rest all = 1
    //从memoryMap中获取指定下标的值
    byte val = value(id);
    //如果根节点的值就已经大于d了,说明剩余内存无法分配
    if (val > d) { // unusable
        return -1;
    }
    //如果节点值小于d,那么表示有足够的容量分配
    //注意这里有一个表达式 (id & initial) == 0 ,这个表达式用于判断当前id是否小于第 d 层第一个节点在 memoryMap 的下标
    while (val < d || (id & initial) == 0) { // id & initial == 1 << d for all ids at depth d, for < d it is 0
        //左子节点,d层的第一个节点的下标为2的d次方,下一层也就是子节点那层的第一个节点的下标为2的d+1次方,是d层第一
        //个节点的2倍,现假设d层的任意节点r的位置离d层第一个节点距离为n,那么r的下标为2^d+n,因为是完全二叉树,
        //r这一层前面n个节点都会两个子节点,所以r对应的左子节点所在层离第一个节点的距离为2n,那么就有2^(d+1)+2n
		//正是其父节点的2倍
        id <<= 1;
        val = value(id);
        if (val > d) {
            //右子节点,对于左子节点,其所在memoryMap的下标一定是一个偶数(除根节点外每层的第一个节点的下标为2的d次幂
            //的值),所以第一个bit是0,与1异或就相当于加1得到右子节点的下标,如果是右子节点异或一,那么得到就是左
            //子节点的下标
            id ^= 1;
            val = value(id);
        }
    }
    byte value = value(id);
    assert value == d && (id & initial) == 1 << d : String.format("val = %d, id & initial = %d, d = %d",
            value, id & initial, d);
    //节点被分配之后,将对应节点值设置为12
    setValue(id, unusable); // mark as unusable
    //更新父节点的值
    //(1)
    updateParentsAlloc(id);
    return id;
}

//(1)
private void io.netty.buffer.PoolChunk#updateParentsAlloc(int id) {
    while (id > 1) {
        //计算父节点的下标
        int parentId = id >>> 1;
        byte val1 = value(id);
        byte val2 = value(id ^ 1);
        //比较左右两个子节点的值,取最小值
        byte val = val1 < val2 ? val1 : val2;
        //用最小值更新父节点的值
        setValue(parentId, val);
        //继续往上更新
        id = parentId;
    }
}

按照以上方法我们用文字描述一遍,假设此时我们要分配一个3M的内存

  • 将3M转换成最接近3M的2的n次幂的值--》4M
  • 计算4M所在层数d--》maxOrder - (log2(4M) - log2(8k)) = 11 - (22 - 13) = 2
  • 从根节点开始,从左子节点到右子节点进行遍历,比较每个节点上的值(存在memoryMap的层数)与2进行比较
  • 左子节点的层数大于2,那么说明这个节点以下没有足够的内存分配,那么找到右节点R进行比较
  • 右节点R的层数小于2,那么说明这个节点以下有足够的内存进行分配,那么继续比较右节点R的左子节点的层数,以此类推,直到找到等于2的那个节点并且这个节点所在数组
    memoryMap的下标不能小于4M所在层数第一个节点在数组memoryMap中的下标,这是因为在分配了一个节点后,父节点的值由两个子节点的最小值决定,所以当父节点的值不等于
    自身所在层数时,其值来源于子节点
  • 找到层数为2的节点C后,将这个节点C的值修改为12,表示这个节点C已经被分配(以后分配的其他任何内存的层数都会小于等于11,所以这里标记成大于11的值都可以)
  • 由于C被分配,那么这个C的父节点还剩下右节点可以分配,那么父节点就需要将值修改为右子节点的值,然后以此类推,直到根节点

具体的图解过程请移步到《netty的内存池设计》

3.2 分配小于8k的内存

简单回顾下我们在《netty的内存池设计》

中提到的关于小于8k内存分配的原理,netty将其分成了两大类,一类是tiny,另一类是small

tiny:

small:

对于以上两种类型的内存分配,netty都维护了一个叫bitMap的long数组,拿tiny的第2号元素来举例,每个PoolSubPage总共8k,按照16字节划分成512份,bitMap中每个bit位

表示一份,那么需要8个long类型元素来才足够表示,分配内存的时候将bit位设置为1,释放时将bit位恢复为0,下面我们来分析下具体的代码实现

java 复制代码
private long io.netty.buffer.PoolChunk#allocateSubpage(int normCapacity) {
    // Obtain the head of the PoolSubPage pool that is owned by the PoolArena and synchronize on it.
    // This is need as we may add it back and so alter the linked-list structure.
    //8k以下的内存被netty分成了两种类型,一种是tiny,另一种是small,其在PoolArean中表现为两个PoolSubPage数组
    //它是由数组+链表构成的,头节点是一个空的PoolSubPage,不参与内存的分配,这段代码是获取其头节点,用于
    //构建细粒度锁
    PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity);
    int d = maxOrder; // subpages are only be allocated from pages i.e., leaves
    synchronized (head) {
        //从二叉树中获取一个8k的内存
        int id = allocateNode(d);
        if (id < 0) {
            return id;
        }
        
        final PoolSubpage<T>[] subpages = this.subpages;
        final int pageSize = this.pageSize;
        //计算剩余可用内存
        freeBytes -= pageSize;
        //计算在PoolSubpage[] subpages中的下标,如果使用不怎么高效的方式计算下标,会这么计算 |memoryMapIdx - maxSubpageAllocs| 取绝对值,但netty的
        //计算方式为 memoryMapIdx ^ maxSubpageAllocs ,其中 maxSubpageAllocs 为2048,因为一个16M的内存最多只能分配2048个8k内存
        //maxSubpageAllocs是一个2的n次幂的值,属于最后一层(第11层)的第一个节点在memoryMap中的下标,所以的bit位上只有第12位是1
        //同是第11层的值大于2048的,他们的值只在12位以下变动,小于2048的也是在第12位以下的bit位发生变动,而2048第12位以下都是0,进行异或
        //就是保存了第12位以下为1的变动
        int subpageIdx = subpageIdx(id);
        PoolSubpage<T> subpage = subpages[subpageIdx];
        if (subpage == null) {
            //runOffset方法用于计算分配内存在memory中的开始偏移
            subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity);
            subpages[subpageIdx] = subpage;
        } else {
            //初始化
            subpage.init(head, normCapacity);
        }
        return subpage.allocate();
    }
}

首先先从PoolArean中的tiny类型或者small类型PoolSubPage数组中获取一个头节点,用于加锁和插入新的节点,然后从二叉树中分配一个8k的内存,封装成PoolSubPage,然后

存入名为subpages的PoolSubPage数组中,其下标计算方式为 memoryMapIdx ^ maxSubpageAllocs ,为什么这么计算,为什么不直接 |memoryMapIdx - maxSubpageAllocs|

取绝对值?

其中 maxSubpageAllocs 为2048,因为一个16M的内存最多只能分配2048个8k内存maxSubpageAllocs是一个2的n次幂的值,属于最后一层(第11层)的第一个节点在memoryMap中

的下标,所以的bit位上只有第12位是1同是第11层的值大于2048的,他们的值只在12位以下变动,小于2048的也是在第12位以下的bit位发生变动,而2048第12位以下都是0,进

行异或就是保存了第12位以下为1的变动,位运算是计算机能够直接识别的操作方式,可以提高计算效率。

创建好PoolSubpage之后,还需要对其进行初始化

java 复制代码
//head:链表头
//elemSize:对于tiny类型,这个值可以是 16,32,...,480,496。
//对于small类型的,这个值可以是512,1024,2049,4096
void io.netty.buffer.PoolSubpage#init(PoolSubpage<T> head, int elemSize) {
    doNotDestroy = true;
    this.elemSize = elemSize;
    if (elemSize != 0) {
        //计算按elemSize划分8k的份数
        maxNumElems = numAvail = pageSize / elemSize;
        //记录下一个可用bit位
        nextAvail = 0;
        //计算需要多少个Long来表示
        bitmapLength = maxNumElems >>> 6;
        //相当于对64求余,如果有余数,那么需要再加一个long来表示
        if ((maxNumElems & 63) != 0) {
            bitmapLength ++;
        }
        //初始化为0
        for (int i = 0; i < bitmapLength; i ++) {
            bitmap[i] = 0;
        }
    }
    //将当前新创建的PoolSubPage加入到链表中
    //(1)
    addToPool(head);
}

//(1)
private void addToPool(PoolSubpage<T> head) {
    assert prev == null && next == null;
    prev = head;
    next = head.next;
    next.prev = this;
    head.next = this;
}

初始化过程很简单,首先计算一个pageSize可以分成多少份,然后再计算需要多少个Long才足够表示对应的份数,最后将PoolSubPage加入到链表中。

下面我们来看看PoolSubPage的内存分配过程

java 复制代码
long io.netty.buffer.PoolSubpage#allocate() {
    if (elemSize == 0) {
        return toHandle(0);
    }

    if (numAvail == 0 || !doNotDestroy) {
        return -1;
    }
    //获取下一个可用的bitmap索引
    final int bitmapIdx = getNextAvail();
    //除以64,确定可用bit为所在的bitMap数组的下标
    int q = bitmapIdx >>> 6;
    //对64求余,确定这个bit位在下标为q的long元素中的偏移
    int r = bitmapIdx & 63;
    assert (bitmap[q] >>> r & 1) == 0;
    //将对应bit为设置为1,表示已经被分配
    bitmap[q] |= 1L << r;
    //如果当前PoolSubPage全部被分配出去了,那么将当前PoolSubPage从链表中移除
    if (-- numAvail == 0) {
        removeFromPool();
    }
    //将bitmapIdx左移到高32位
    //(1)
    return toHandle(bitmapIdx);
}

//(1)
private long io.netty.buffer.PoolSubpage#toHandle(int bitmapIdx) {
    //将bitmapIdx左移到高32位,memoryMapIdx放在低32位
    //0x4000000000000000L 暂时不知道用于干啥的,用了这个之后,要获取真实的bitmapIdx还得与上 0x3FFFFFFF
    return 0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx;
}

上面一段代码首先先确定可用的下一个bit为索引,然后将对应bit位设置为1,表示已被分配,如果可用的份数全部分配完毕,那么将当前PoolSubPage从链表中移除。

下面再看下具体是怎么找到下一个可用的bit位的

java 复制代码
private int io.netty.buffer.PoolSubpage#getNextAvail() {
    int nextAvail = this.nextAvail;
    //如果提前预读了下一个可用bit位索引,直接返回
    if (nextAvail >= 0) {
        this.nextAvail = -1;
        return nextAvail;
    }
    return findNextAvail();
}

private int io.netty.buffer.PoolSubpage#findNextAvail() {
    final long[] bitmap = this.bitmap;
    final int bitmapLength = this.bitmapLength;
    //遍历bitMap数组
    for (int i = 0; i < bitmapLength; i ++) {
        long bits = bitmap[i];
        //被分配掉的bit为会被置为1,如果整个long元素被分配了,那么取反之后,它的值等于0
        if (~bits != 0) {
            return findNextAvail0(i, bits);
        }
    }
    return -1;
}

private int io.netty.buffer.PoolSubpage#findNextAvail0(int i, long bits) {
    //总份数
    final int maxNumElems = this.maxNumElems;
    //i表示遍历到的第几个long元素,然后乘以64,表示当前i对应的long元素的第一个bit为索引
    final int baseVal = i << 6;
    //遍历当前i对应的long元素的每个bit位
    for (int j = 0; j < 64; j ++) {
        //等于表示没有被分配
        if ((bits & 1) == 0) {
            //相当于 baseVal + j,难道 baseVal | j = baseVal + j?不是的,必须满足baseVal为1的bit位不会与j中为1的bit位进行位或
            //很显然j是一个小于64 = 1 << 6的值,其为1的bit位在低6位上,而baseVal要么是零,要么就是 i << 6(i > 0),其为1的bit位不会出现在低6位上
            int val = baseVal | j;
            if (val < maxNumElems) {
                return val;
            } else {
                break;
            }
        }
        //移位
        bits >>>= 1;
    }
    return -1;
}

遍历bitMap数组中的每个long类型的元素,然后判断其bit位是否都已经被分配出去了,如果没有,那么遍历其每个bit位,找到为零的那个bit,返回bit索引即可。

四、内存的释放

java 复制代码
void io.netty.buffer.PoolChunk#free(long handle, ByteBuffer nioBuffer) {
    // (int)handle,memoryMapIdx存在与handle的低32位
    int memoryMapIdx = memoryMapIdx(handle);
    //高32
    int bitmapIdx = bitmapIdx(handle);
    //bitmapIdx不为零,那么表示当前要释放的是小于8k的内存
    if (bitmapIdx != 0) { // free a subpage
        //subpageIdx方法用于计算对应PoolSubPage的下标
        PoolSubpage<T> subpage = subpages[subpageIdx(memoryMapIdx)];
        assert subpage != null && subpage.doNotDestroy;

        // Obtain the head of the PoolSubPage pool that is owned by the PoolArena and synchronize on it.
        // This is need as we may add it back and so alter the linked-list structure.
        //在PoolArean中判断分配类型,tiny还是small,然后获取对应PoolSubPage的头节点
        PoolSubpage<T> head = arena.findSubpagePoolHead(subpage.elemSize);
        synchronized (head) {
            //bitmapIdx & 0x3FFFFFFF 还原真实的bit索引
            //释放
            if (subpage.free(head, bitmapIdx & 0x3FFFFFFF)) {
                return;
            }
        }
    }
    //runLength方法用于计算对应memoryMapIdx表示的内存大小
    freeBytes += runLength(memoryMapIdx);
    //将二叉树对应的节点恢复为原来的层数值,层数值从deptMap数组中获取
    setValue(memoryMapIdx, depth(memoryMapIdx));
    //更新父节点的值
    updateParentsFree(memoryMapIdx);
    //缓存nioBuffer
    if (nioBuffer != null && cachedNioBuffers != null &&
            cachedNioBuffers.size() < PooledByteBufAllocator.DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK) {
        cachedNioBuffers.offer(nioBuffer);
    }
}

如果是小于8k的内存释放,那么需要先将PoolSubPage中bitMap对应的bit为重置为0,然后再释放二叉树树中的节点

下面便是PoolSubPage的释放方法

java 复制代码
boolean io.netty.buffer.PoolSubpage#free(PoolSubpage<T> head, int bitmapIdx) {
    if (elemSize == 0) {
        return true;
    }
    //计算bit索引为在bitMap数组中的下标
    int q = bitmapIdx >>> 6;
    //计算指定q元素中long的bit位索引
    int r = bitmapIdx & 63;
    assert (bitmap[q] >>> r & 1) != 0;
    //将对应的bit位重置为0
    bitmap[q] ^= 1L << r;
    //记录将当前释放的bit位索引,下次分配内存时可快速分配
    setNextAvail(bitmapIdx);
    //递增可用份数
    if (numAvail ++ == 0) {
        //重新加入链表
        addToPool(head);
        return true;
    }

    if (numAvail != maxNumElems) {
        return true;
    } else {
        // Subpage not in use (numAvail == maxNumElems)
        //只剩最后一个PoolSubPage,不会被移除,netty需要留一个头节点构建链表
        if (prev == next) {
            // Do not remove if this subpage is the only one left in the pool.
            return true;
        }

        // Remove this subpage from the pool if there are other subpages left in the pool.
        doNotDestroy = false;
        //从链表中移除
        removeFromPool();
        return false;
    }
}

PoolSubPage的释放很简单,就是将原来分配了内存的bit位由1变回0

五、PooledByteBuf的初始化

分配好内存之后,需要初始化PooledByteBuf,告诉它,它所能管辖的内存范围是memory中的哪一部分

java 复制代码
void io.netty.buffer.PoolChunk#initBuf(PooledByteBuf<T> buf, ByteBuffer nioBuffer, long handle, int reqCapacity) {
    // (int) handle
    int memoryMapIdx = memoryMapIdx(handle);
    // handle >>> 32
    int bitmapIdx = bitmapIdx(handle);
    if (bitmapIdx == 0) {
        byte val = value(memoryMapIdx);
        assert val == unusable : String.valueOf(val);
        //8k以上内存的初始化
        buf.init(this, nioBuffer, handle, runOffset(memoryMapIdx) + offset,
                reqCapacity, runLength(memoryMapIdx), arena.parent.threadCache());
    } else {
        //8k以下内存的初始化
        initBufWithSubpage(buf, nioBuffer, handle, bitmapIdx, reqCapacity);
    }
}

PooledByteBuf的初始化无非就是设置其负责memory的内存范围,我们重点研究一下runOffset方法和runLength方法

runLength方法用于计算对应id表示的内存大小

java 复制代码
private int runLength(int id) {
    // represents the size in #bytes supported by node 'id' in the tree
    //depth(id)返回层数,比如第11层
    //log2ChunkSize的值为24
    //我们计算层数的时候是这么计算的
    // 11 - (log2(x) - log2(8k)) 那么反过来我们要求x ==》 1 << (11 - depth(id) + log2(8k)) ==》 1 << (11 - depth(id) + 13) ==> 1 << (24 - depth(id))
    return 1 << log2ChunkSize - depth(id);
}

1 << log2ChunkSize - depth(id)用于计算当前id对应层数的内存大小,其中log2ChunkSize的值为24,当初我们计算层数的时候是这么计算的 11 - (log2(x) - log2(8k))

现在反过来我们要求x的值 ==》 1 << (11 - depth(id) + log2(8k)) ==》 1 << (11 - depth(id) + 13) ==> 1 << (24 - depth(id))

runOffset方法用于计算内存偏移

java 复制代码
private int io.netty.buffer.PoolChunk#runOffset(int id) {
    // represents the 0-based offset in #bytes from start of the byte-array chunk
    int shift = id ^ 1 << depth(id);
    return shift * runLength(id);
}

depth(id)获取id对应的层数,记为L

1 << L 用于计算第L层第一个节点在memoryMap的下标,记为memoryIdx,那么id与memoryIdx相差 id - memoryIdx,但是netty并没有直接这样计算,它是通过 id ^ memoryIdx

位操作计算的,为什么可以这样计算?\

前面也提到过,要满足公式 |a - b| = a ^ b,那么a为1的bit位不能和b为1的bit位发生异或操作,由于memoryIdx是一个2的n次幂的值,那么其低n位都是0,而id与memoryIdx

处于同一L层,那么id的值与memoryIdx相比,其发生变化的bit位都在低n位,所以可以直接通过异或的方式获取其偏移位置

六、总结

就是其原理的总结:《netty的内存池设计》

相关推荐
蓝染-惣右介16 分钟前
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
java·设计模式
秋恬意1 小时前
IBatis和MyBatis在细节上的不同有哪些
java·mybatis
齐 飞1 小时前
BeanFactory和FactoryBean
java·sprint
大霞上仙2 小时前
lxml 解析xml\html
java·服务器·网络
Xiaoweidumpb2 小时前
tomcat temp临时文件不清空,占用硬盘,jdk字体内存泄漏
java·tomcat
AI人H哥会Java2 小时前
【Spring】控制反转(IoC)与依赖注入(DI)—IoC容器在系统中的位置
java·开发语言·spring boot·后端·spring
不能只会打代码2 小时前
Java并发编程框架之综合案例—— 分布式日志分析系统(七)
java·开发语言·分布式·java并发框架
自律的kkk2 小时前
SpringBoot中使用AOP切面编程实现登录拦截
java·spring boot·aop·切面编程·登录拦截
丁总学Java2 小时前
nohup java -jar productQualification.jar --spring.profiles.active=prod $
java·spring·jar