Netty的内存分配优化源码解析

639 阅读21分钟

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

Netty 作为一款高性能的网络框架,需要处理海量的字节数据,而且 Netty 默认提供了池化对象的内存分配,使用完后归还到内存池,所以一套高性能的内存管理机制是 Netty 必不可少的。Netty是如何对内存进行优化的呢?

一、内存的种类

Netty中的内存类型总的可以归为两大类,六小类!我们查看他的类图:

image-20210515171241565

我们可以看到,这里的内存的种类是非常多的,不同的种类有八种之多,我们通过三个维度对他进行介绍:

1. Pooled和Unpooled

我们从上图的命名上也可以看到这里的,基本都是以Pooledxxxx和Unpooledxxxx命名开头的。顾名思义:

以Pooledxxxx命名的是池化内存,是从预先分配好的内存里取一块内存进行使用,使用完毕后进行归还!

2. Unsafe和非Unsafe

有关于unsafe的概念我们在前几章进行了分析,他是能够操作物理内存的地址,进行内存的分配!这里Netty也会区分两种形式,当操作系统里面能够获取到Unsafe的时候就使用Unsafe,不存在的时候就使用非Unsafe!

3. Heap和Direct

一个是堆内内存,一个是堆外内存,我们前面也分析过,堆内内存主要是使用Java的byte数组进行操作,堆外内存是直接分配物理内存,返回一个物理内存的地址,通过地址进行操作内存!

4. 汇总

经过上述的介绍,我们将各种类型的ByteBuf进行组合,能够组成8种不同功能的:

池化的Unsafe的堆内内存:PooledUnsafeHeapByteBuf

池化的Unsafe的堆外内存:PooledUnsafeDirectByteBuf

池化的非Unsafe的堆内内存:PooledHeapByteBuf

池化的非Unsafe的堆外内存:PooledDirectByteBuf

非池化的Unsafe的堆内内存:UnpooledUnsafeHeapByteBuf

非池化的Unsafe的堆外内存:UnpooledUnsafeDirectByteBuf

非池化的非Unsafe的堆内内存:UnpooledHeapByteBuf

非池化的非Unsafe的堆外内存:UnpooledDirectByteBuf

二、内存分配器的主要API

/**
 * 实现负责分配缓冲区。预计该接口的实现是
 * 线程安全的。
 */
public interface ByteBufAllocator {

    ByteBufAllocator DEFAULT = ByteBufUtil.DEFAULT_ALLOCATOR;

    ................................................................
    /**
     * 分配堆{@link ByteBuf}。
     */
    ByteBuf heapBuffer();
    ................................................................
    /**
     *分配直接的{@link ByteBuf}。
     */
    ByteBuf directBuffer();
    ................................................................
 }

可以看到主要API有两个 heapBufferdirectBuffer 这里体现了两个维度: Heap和Direct,那么其他维度怎么体现的呢?我们进入到他的实现类AbstractByteBufAllocator:

public abstract class AbstractByteBufAllocator implements ByteBufAllocator{
    ................................................
    //分配一个堆内内存
    @Override
    public ByteBuf heapBuffer(int initialCapacity, int maxCapacity) {
        if (initialCapacity == 0 && maxCapacity == 0) {
            return emptyBuf;
        }
        validate(initialCapacity, maxCapacity);
        return newHeapBuffer(initialCapacity, maxCapacity);
    }
    ..................................................
    protected abstract ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity);
    
    //分配一个堆外内存
    @Override
    public ByteBuf directBuffer(int initialCapacity, int maxCapacity) {
        if (initialCapacity == 0 && maxCapacity == 0) {
            return emptyBuf;
        }
        validate(initialCapacity, maxCapacity);
        //子类又 polled和Unpolled
        return newDirectBuffer(initialCapacity, maxCapacity);
    }
    .....................................................
    protected abstract ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity);
}

我么可以看到两个实现类最终调用的方法:newHeapBuffernewDirectBuffer, 都是抽象方法,具体的实现交给子类来实现,我们查看以下他的子类有哪些:

image-20210515192618965

至此,又一个维度被我们分析出来了,判断一个类是否需要池化,需要依赖于子类的实现,我们现在为止分析出来了两个维度:Heap、Direct和Pooled和Unpooled,按照上述的分析,我们还少了一个维度 :Unsafe和非Unsafe,它是如何区分出来的呢? 我们下述的讲解将以池化内存的堆外内存的分配作为基本讲解,因为他比较复杂,至于非池化的缓存的分配,就留一个作业,同学们课下分析,我们进入到PooledByteBufAllocator :

三、源码分析

public class PooledByteBufAllocator extends AbstractByteBufAllocator implements ByteBufAllocatorMetricProvider {
    .....................................................................
    @Override
    protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
        //从当前线程中获取一个 线程缓存池
        PoolThreadCache cache = threadCache.get();
        PoolArena<ByteBuffer> directArena = cache.directArena;

        final ByteBuf buf;
        if (directArena != null) {
            buf = directArena.allocate(cache, initialCapacity, maxCapacity);
        } else {
            buf = PlatformDependent.hasUnsafe() ?
                    UnsafeByteBufUtil.newUnsafeDirectByteBuf(this, initialCapacity, maxCapacity) :
                    new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
        }

        return toLeakAwareBuffer(buf);
    }
    .....................................................................
}

其实我们乍一看这个逻辑不咋复杂,我们暂时不考虑他具体是如何实现的,我们单看这个方法,主线逻辑:

  1. 从一个类似缓存的东西里面取出了一个PoolArena
  2. 然后使用PoolArena进行分配内存,然后返回分配好的buf

我们通过查看构造方法可以知道,directArena对应的是DirectArena对象

我们查看 buf = directArena.allocate(cache, initialCapacity, maxCapacity);的源码:

PooledByteBuf<T> allocate(PoolThreadCache cache, int reqCapacity, int maxCapacity) {
    //获取一个服复用的
    PooledByteBuf<T> buf = newByteBuf(maxCapacity);
    allocate(cache, buf, reqCapacity);
    return buf;
}

这里面分为两个步骤:

  1. 创建或者获取一个PooledByteBuf

    image-20210515224254232

    @Override
    protected PooledByteBuf<ByteBuffer> newByteBuf(int maxCapacity) {
        if (HAS_UNSAFE) {
            return PooledUnsafeDirectByteBuf.newInstance(maxCapacity);
        } else {
            return PooledDirectByteBuf.newInstance(maxCapacity);
        }
    }
    

    HAS_UNSAFE:就是判断系统内是否存在Unsafe对象,存在的话就创建一个PooledUnsafeDirectByteBuf不存在就创建一个PooledDirectByteBuf!至此我们的第三个维度出来了!第三个维度是否使用Unsafe是由系统自动判别的!

    我们进入到他的源码:

    static PooledUnsafeDirectByteBuf newInstance(int maxCapacity) {
        PooledUnsafeDirectByteBuf buf = RECYCLER.get();
        //进行复用  重设数据
        buf.reuse(maxCapacity);
        return buf;
    }
    

    这里用到了一个概念:Recycler 对象池技术 比较简单,当我们创建一个对象,使用完毕之后,为了下一次使用不重复创建,会将其压入自己的栈中,避免被GC掉,下次使用的时候直接就从栈中获取,不再重复创建了!

    我们查看下RECYCLER

    private static final ObjectPool<PooledUnsafeDirectByteBuf> RECYCLER = ObjectPool.newPool(
                new ObjectCreator<PooledUnsafeDirectByteBuf>() {
                    @Override
                    public PooledUnsafeDirectByteBuf newObject(Handle<PooledUnsafeDirectByteBuf> handle) {
                        return new PooledUnsafeDirectByteBuf(handle, 0);
                    }
                });
    

    这里代码的逻辑是,当从栈内部获取的复用对象为空即不存在该复用对象的时候,就创建一个PooledUnsafeDirectByteBuf对象并返回!

    buf.reuse(maxCapacity);
    

    因为我们在上一步获取的PooledUnsafeDirectByteBuf可能是上一次用过的,所以我们需要将里面的读写指针归0到初始位置!

    final void reuse(int maxCapacity) {
        //重置最大容量
        maxCapacity(maxCapacity);
        //重置引用
        resetRefCnt();
        //重置读写指针
        setIndex0(0, 0);
        //重置标识
        discardMarks();
    }
    
  2. 给获取出来的PooledByteBuf分配内存

    allocate(cache, buf, reqCapacity);
    

    开始分配内存,在Netty中,Netty对于不同大小的内存分配,设定了不同的分配方式,将Netty的内存规格介绍一下:

    image-20210516104132168

    Netty对于分配16B496B之间的内存称之为Tiny, 对于分配512B4K之间的内存称之为Small,对于分配8K~16M的内存称之为norm,对于16M的统一称之为Huge!

    每次分配内存,Netty如果发现分配的内存的大小,不是2的次幂,就会向上取整,到内存规格的大小,譬如:我们分配了一个10B大小的内存,Netty会自动将之转换为16B即2的4次方,属于Tiny类型的数据!

    在Netty规格种类中分为了三个角色:

    Chunk: 在Netty中会一次性分配一个16M的内存大小,Chunk是一次分配的内存是16M! 后续所有的内存分配,就在Chunk上分配!

    Page: 在Netty中以Page作为单位,Netty将Chunk以Page的大小且深度为11的满二叉树:

    image-20210516121809222

    例如:当我们分配一个 32K的内存的时候,系统就会寻找两个相邻的4个Page进行返回!

    SubPage: 当我们分配的内存小于8K的时候,此时如果直接分配一个8K的内存就会造成严重的空间浪费,所以,当我们分配的内存小于一个Page的时候,就会将一个Page划分为等份的SubPage!

    即当我们分配一个1K 的空间大小的时候,就会从满二叉树向下寻找到第一个8K的内存块,然后将其等分为8份!返回该ByteBuffer,并将该满二叉树逐层向上修改(这都是后话)!

    我们了解了Netty分配不同规格的内存大小之后,我们对其源码进行分析! 我们进入到 allocate(cache, buf, reqCapacity);

    private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) {
            //规范化容量 第一是向上取规范容量并返回
            final int normCapacity = normalizeCapacity(reqCapacity);
            // capacity < pageSize   分配内存小于8k
            if (isTinyOrSmall(normCapacity)) {
                int tableIdx;
                PoolSubpage<T>[] table;
                boolean tiny = isTiny(normCapacity);
                if (tiny) { // < 512
                    //缓存上分配
                    if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {
                        // 能够分配出缓存,所以继续
                        return;
                    }
                    //除16  能得到应该分配到 tiny数组的位置  16  32 48 64.......496   都是16的倍数
                    tableIdx = tinyIdx(normCapacity);
                    table = tinySubpagePools;
                } else {
                    if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) {
                        // 能够分配出缓存,所以继续
                        return;
                    }
                    tableIdx = smallIdx(normCapacity);
                    table = smallSubpagePools;
                }
    
                final PoolSubpage<T> head = table[tableIdx];
    
                /**
                 * Synchronize on the head. This is needed as {@link PoolChunk#allocateSubpage(int)} and
                 * {@link PoolChunk#free(long)} may modify the doubly linked list as well.
                 */
                synchronized (head) {
                    final PoolSubpage<T> s = head.next;
                    if (s != head) {
                        assert s.doNotDestroy && s.elemSize == normCapacity;
                        long handle = s.allocate();
                        assert handle >= 0;
                        s.chunk.initBufWithSubpage(buf, null, handle, reqCapacity);
                        incTinySmallAllocation(tiny);
                        return;
                    }
                }
                synchronized (this) {
                    //直接分配
                    allocateNormal(buf, reqCapacity, normCapacity);
                }
    
                incTinySmallAllocation(tiny);
                return;
            }
    
            //这里分配的是page  <= 16M
            if (normCapacity <= chunkSize) {
                //
                if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {
                    // was able to allocate out of the cache so move on
                    return;
                }
                synchronized (this) {
                    //实际的内存分配
                    allocateNormal(buf, reqCapacity, normCapacity);
                    ++allocationsNormal;
                }
            } else {
                // Huge allocations are never served via the cache so just call allocateHuge
                allocateHuge(buf, reqCapacity);
            }
        }
    

1. Page级别的内存分配

我们假设现在分配: 8193B的内存:

ByteBuf byteBuf = PooledByteBufAllocator.DEFAULT.directBuffer(8*1024 + 1);

final int normCapacity = normalizeCapacity(reqCapacity);

将我们的数据向上取整,我们现在分配的是 8193B内存,那么经过该行代码之后就会变成16384B即16K

if (isTinyOrSmall(normCapacity))

判断我们分配的内存是否是 Tiny或者Small类型的,显然并不是,因为他大于8k,所以不会进这个判断!

if (normCapacity <= chunkSize) {
    //先尝试从当前的线程里面取一次,取到的话直接返回
    if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {
        // was able to allocate out of the cache so move on
        return;
    }
    synchronized (this) {
        //实际的内存分配
        allocateNormal(buf, reqCapacity, normCapacity);
        ++allocationsNormal;
    }
} else {
    // 如果分配的内存大于16M  那么直接从内存分配一块Huge类型的,不走缓存
    allocateHuge(buf, reqCapacity);
}

我们前面进行过分析,一个chunkSize,是16M,所以当我们分配的内存大于等于8K,小于16M的时候,会进入到这个逻辑,分配一个Page类型的内存!

  1. 先尝试从当前的线程中取出一块合适的内存,我们现在是第一次分配,这里当然会返回false

  2. 开始进行实际的内存分配!

    allocateNormal(buf, reqCapacity, normCapacity);
    
     //大于8k  从q050开始  链表上没有对应的PoolChunk
    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;
    }
    

    这里又要普及一个概念,在DirectArena中构建了5个PoolChunkList,他们的内存结如下:

    image-20210516130504895

    每一个PoolChunkList都存放一个对应的使用率的大小的内存!我们都知道一个PoolChunk是16M,内部进行了各种的Page和SubPage之后,使用率也会提升,所以Netty维护了6个队列,进行不同使用率的分配!

    • qInit: PoolChunk的使用率达到0%-25%的!

    • q000: PoolChunk的使用率达到1%-50%的!

    • q025: PoolChunk的使用率达到25%-75%的!

    • q050: PoolChunk的使用率达到50%-100%的!

    • q075: PoolChunk的使用率达到75%-100%的!

    • q100: PoolChunk的使用率达到100%的!

所以这里代码的意思是,先从使用率为50%的开始寻找,如果能够找到合适的chunk进行内存分配就直接返回,如果寻找不到,就继续向下执行开始真正的内存分配!我们首次进行内存分配,这里不会存在合适的内存,所以我们继续向下执行!

// 添加一个新的块。  创建PoolChunk  内涵平衡二叉树
PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);

首先创建一个PoolChunk,我们查看创建的时候做了什么!

  1.  @Override
    protected PoolChunk<ByteBuffer> newChunk(int pageSize, int maxOrder,
                                             int pageShifts, int chunkSize) {
        if (directMemoryCacheAlignment == 0) {
            return new PoolChunk<ByteBuffer>(this, allocateDirect(chunkSize), pageSize, maxOrder, pageShifts, chunkSize, 0);
        }
        .....................一般不会进入到这个分支,directMemoryCacheAlignment默认为0..........................
    }
    
    • 首先他会调用JDK的方式,分配一块16M的内存:

    • allocateDirect(chunkSize)
      
    •  private static ByteBuffer allocateDirect(int capacity) {
           return PlatformDependent.useDirectBufferNoCleaner() ?
               PlatformDependent.allocateDirectNoCleaner(capacity) : ByteBuffer.allocateDirect(capacity);
       }
      
    • 这段代码相信大家不会陌生,调用JDK的方式分配一块堆外内存!分配的大小是一个chunkSize 即16M

  2. PoolChunk(PoolArena<T> arena, T memory, int pageSize, int maxOrder, int pageShifts, int chunkSize, int offset) {
        this.arena = arena;
        //默认16M的内存块
        this.memory = memory;
        //默认8K
        this.pageSize = pageSize;
        //默认11   maxSubpageAllocs = 1 << maxOrder
        //可得一个PoolChunk默认情况下由2^11=2048个SubPage构成,而默认情况下一个page默认大小为8k,即pageSize=8K。
        unusable = (byte) (maxOrder + 1);
        //空闲内存 16M
        freeBytes = chunkSize;
        //右移11层
        maxSubpageAllocs = 1 << maxOrder; 
        // 生成内存映射。
        //PoolChunk中所有的PoolSubpage都放在PoolSubpage[] subpages中,为了更好的分配,Netty用一颗平衡二叉树记录每个PoolSubpage的分配情况   2048
        memoryMap = new byte[maxSubpageAllocs << 1];//已使用的内存  11层 满二叉树为4096个节点
        depthMap = new byte[memoryMap.length];//节点深度
        //开始构建一个平衡二叉树
        int memoryMapIndex = 1;
        // 一次向下移动树一级  maxOrder 树的最大层级
        for (int d = 0; d <= maxOrder; ++ d) {
            //深度 计算深度
            int depth = 1 << d;
            //memoryMap={第0位没用到,0,1,1,2,2,2,2,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4},//memoryMap数组元素长度为 {(1<<maxOrder)>>1}}=32
            for (int p = 0; p < depth; ++ p) {
                // 在每个级别中从左到右遍历并将值设置为子树的深度
                memoryMap[memoryMapIndex] = (byte) d;
                //使用情况
                depthMap[memoryMapIndex] = (byte) d;
                memoryMapIndex ++;
            }
        }
        //创建 PollSubpage
        subpages = newSubpageArray(maxSubpageAllocs);
    }
    
    • 创建PoolChunk比较重要的一段代码是创建了一个平衡二叉树:

    • memoryMap = new byte[maxSubpageAllocs << 1];//已使用的内存  11层 满二叉树为4096个节点
      depthMap = new byte[memoryMap.length];//节点深度
      //开始构建一个平衡二叉树
      int memoryMapIndex = 1;
      // 一次向下移动树一级  maxOrder 树的最大层级
      for (int d = 0; d <= maxOrder; ++ d) {
          //深度 计算深度
          int depth = 1 << d;
          //memoryMap={第0位没用到,0,1,1,2,2,2,2,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4..........}
          //memoryMap数组元素长度为4096
          for (int p = 0; p < depth; ++ p) {
              // 在每个级别中从左到右遍历并将值设置为子树的深度
              memoryMap[memoryMapIndex] = (byte) d;
              //使用情况
              depthMap[memoryMapIndex] = (byte) d;
              memoryMapIndex ++;
          }
      }
      
    • maxSubpageAllocs:2048, <<1 = 2048 * 2 = 4096, 所以这里memoryMap和depthMap的长度为4096个长度!这里的代码的意思是构建出来了一个平衡满二叉树:

    • image-20210516203749443

      该图 对应上图的满二叉树即从16M到8K的内存分配!

Chunk创建完毕后,开始使用新创建的PoolChunk进行内存分配!

//使用 PoolChunk进行分配内存  给ByteBuf
boolean success = c.allocate(buf, reqCapacity, normCapacity);
boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
    final long handle;
    // >= pageSize   大于等于8k
    if ((normCapacity & subpageOverflowMask) != 0) {
        //返回分配的节点的id   平衡二叉树中的空闲的节点id
        handle =  allocateRun(normCapacity);
    } else {
        //subpage
        handle = allocateSubpage(normCapacity);
    }
    //没有分配成功
    if (handle < 0) {
        return false;
    }
    ByteBuffer nioBuffer = cachedNioBuffers != null ? cachedNioBuffers.pollLast() : null;
    //初始化内存
    initBuf(buf, nioBuffer, handle, reqCapacity);  
    return true;
}

我们先看第一个判断:

if ((normCapacity & subpageOverflowMask) != 0)

判断我们要分配的内存是否大于8K ,大于8K就是使用Page的方式进行分配!否则就按照SubPage的方式分配,我们这里分配了16K的内存,所以这里使用Page的方式进行分配:

handle =  allocateRun(normCapacity);

这里会寻找上述我们构建的满二叉树,寻找一个合适的节点,将对应的Id返回过来!

private long allocateRun(int normCapacity) {
    //计算平衡二叉树的深度  对应分配内存大小的二叉树深度
    int d = maxOrder - (log2(normCapacity) - pageShifts);
    //开始给该需求容量分配一个节点  查询到节点
    int id = allocateNode(d);
    //没有分配成功
    if (id < 0) {
        return id;
    }
    //空闲内存-刚刚分配的内存   16M - 分配的规范内存
    freeBytes -= runLength(id);
    //这里返回的id一定是平衡二叉树中未使用的内存空间
    return id;
}

这里分为三个步骤:

  1. 计算当前这个分配的大小,对应满二叉树的深度!

    int d = maxOrder - (log2(normCapacity) - pageShifts);
    

    上述的表达式对应: 11 - (log2(16384) - 13) = 10, 最底层的叶子节点为8K,其父级一定是16K.也就是第十层!

  2. 在对应深度上寻找一个对应大小的节点,返回他的id!

    int id = allocateNode(d);
    
    /**
     * 查询深度d的空闲节点时在memoryMap中分配索引的算法
     *
     * @param d 深度
     * @return memoryMap中的索引
     */
    private int allocateNode(int d) {
        int id = 1;
        int initial = - (1 << d); // 最后d位= 0,其余全部= 1
        byte val = value(id);
        // 无法使用
        if (val > d) { 
            return -1;
        }
        //开始遍历平衡二叉树寻找可用的节点
        while (val < d || (id & initial) == 0) { // id&initial == 1 << d对于深度d的所有id,对于<d为0
            id <<= 1;
            val = value(id);
            //这里注意父级节点的状态的修改是取的子节点的最小值,当子节点全部分配完毕之后,父节点也会变,就会进入到判断逻辑里面
            if (val > d) {
                //如果寻找到的节点分配过了,这里会向后移动一位
                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);
        setValue(id, unusable); // 标记为不可用
        //修改父级节点
        updateParentsAlloc(id);
        return id;
    }
    

    具体的寻找逻辑如下:

    先取出根节点的值,判断是否已经被使用过,如果被使用过直接返回-1!

    开始遍历平衡二叉树,从根节点开始向下寻找,直到直到对应的深度的节点,如果中间发现某一结点已经被使用就后移寻找下一节点的子节点!

    寻找到之后将之标记位不可用,即标记为12,然后遍历父节点,逐层向上修改!父节点的值等于子节点的最小值,如下图所示!

    image-20210516205848213

    这样就寻找到对应的节点了,对应的节点的Id为 1024!

    当我们再次分配一个16K 的节点的时候,还是寻找到10,取出1024的值为12,大于10,所以使用id ^= 1;得到1025,取出1025的值为10,不大于10,所以返回1025,同时修改本节点为12,再次逐层向上修改:

    image-20210516211501227

  3. 减少空闲内存的余量

    //空闲内存-刚刚分配的内存   16M - 分配的规范内存
    freeBytes -= runLength(id);
    

    重新计算空闲内存,使用chunkSize - 分配内存 = 16M - 16K

我们上述过程就寻找到了对应的节点的id,所以继续向下分配内存!

initBuf(buf, nioBuffer, handle, reqCapacity);  //初始化内存
void initBuf(PooledByteBuf<T> buf, ByteBuffer nioBuffer, long handle, int reqCapacity) {
    //再平衡二叉树上找的节点id
    int memoryMapIdx = memoryMapIdx(handle);
    int bitmapIdx = bitmapIdx(handle);
    if (bitmapIdx == 0) {
        //查询是否为不可用  不可用就对了
        byte val = value(memoryMapIdx);
        assert val == unusable : String.valueOf(val);
        buf.init(this, nioBuffer, handle, runOffset(memoryMapIdx) + offset,
                 reqCapacity, runLength(memoryMapIdx), arena.parent.threadCache());
    } else {
        initBufWithSubpage(buf, nioBuffer, handle, bitmapIdx, reqCapacity);
    }
}

这里我们分配的是Page所以 bitmapIdx = 0!所以会走buf.init(......)方法,开始初始化byteBuf,但是我们试想一个事情,我们的ByteBuf是多大,是16M,但是我们只需要分配16K的空间,想要多个16K复用同一个ByteBuf的话,就必须要有一个记录该段小内存的起始位置的地方,这个地方叫做offset!

我们看buf.init里面的runOffset(memoryMapIdx) + offset , 第一次分配的话得出的结果为0, 假如我们由分配一次16K的内存,那么该offset的偏移量就是16384! 如图所示:

第一次分配内存 (分配规格16K):

image-20210516214942493

第二次分配(分配规格16K):

image-20210516215438100

Netty就是使用上述的方式进行两个PoolByteBuf复用同一个ByteBuffer的!我们进入到初始化的源码中!

@Override
void init(PoolChunk<ByteBuffer> chunk, ByteBuffer nioBuffer,
          long handle, int offset, int length, int maxLength, PoolThreadCache cache) {
    super.init(chunk, nioBuffer, handle, offset, length, maxLength, cache);
    //初始化内存地址
    initMemoryAddress();
}

这里总共分为两个步骤:

  1. 初始化ByteBuf分配内存

    • super.init(chunk, nioBuffer, handle, offset, length, maxLength, cache);
      
    • void init(PoolChunk<T> chunk, ByteBuffer nioBuffer, long handle, int offset, 
                int length, int maxLength, PoolThreadCache cache) {
          init0(chunk, nioBuffer, handle, offset, length, maxLength, cache);
      }
      
    • private void init0(PoolChunk<T> chunk, ByteBuffer nioBuffer,
                             long handle, int offset, int length, int maxLength, PoolThreadCache cache) {
          assert handle >= 0;
          assert chunk != null;
          //开始初始化PollByteBuf的使用
          this.chunk = chunk;
          //对应的ChunkByteBuffer
          memory = chunk.memory;
          tmpNioBuf = nioBuffer;
          allocator = chunk.arena.parent;
          //对应的线程缓存
          this.cache = cache;
          //来内需的内存空间地址
          this.handle = handle;
          //对应的偏移量
          this.offset = offset;
          //对应的长度
          this.length = length;
          //最大长度
          this.maxLength = maxLength;
      }
      
    • 所谓的PooledUnsafeDirterByteBuf的初始化就是对各种值进行赋值,完成初始化!

  2. 初始化地址,因为是基于Unsafe进行分配的!

    //初始化内存地址
    initMemoryAddress();
    
    private void initMemoryAddress() {
        memoryAddress = PlatformDependent.directBufferAddress(memory) + offset;
    }
    

    计算当前对象的地址 加上当前小对象的偏移量 = 本段小ByteBuf的地址!

    至此,一个Page级别的ByteBuf就创建完成了!

2. SubPage的内存分配

了解了上述的Page级别的分配之后,我们再次一起学习SubPage的分配方式!

场景:我们分配了一个 12B的内存

ByteBuf byteBuf = PooledByteBufAllocator.DEFAULT.directBuffer(12);

12B向上取整为16K

我们回到代码最初的位置PoolArena#allocate(io.netty.buffer.PoolThreadCache, io.netty.buffer.PooledByteBuf<T>, int):

if (isTinyOrSmall(normCapacity)) {....}

这里判断分配的内存是否小于8K,当然是小于的,所以进入逻辑:

boolean tiny = isTiny(normCapacity);

这里判断是否小于512K,当然也小于,所以返回true

if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {
    // 能够分配出缓存,所以继续
    return;
}
//除16  能得到应该分配到 tiny数组的位置  16  32 48 64.......496   都是16的倍数
tableIdx = tinyIdx(normCapacity);
table = tinySubpagePools;
  1. 尝试从线程缓存中获取一个合适的ByteBuf,我们第一次分配该大小的内存,所以缓存没有,就返回false

  2. 算出当前的空间大小的内存属于第几个位置,:

    我们之前说到过,我们对于Tiny的分配如下图所示:

    image-20210516222836799

即32个内存种类,Netty的PoolArena中维护了两个数组,分别是:

private final PoolSubpage<T>[] tinySubpagePools; //32
private final PoolSubpage<T>[] smallSubpagePools; //4

tinySubpagePools的内存结构如下所示:

image-20210517084815160

他在构造函数中初始化,这里的tinyIdx方法就是计算我们当前所分配的大小属于在内存块的哪一个地方,我们进入到 tinyIdx(normCapacity);

static int tinyIdx(int normCapacity) {
    return normCapacity >>> 4;
}

逻辑很简单就是直接拿我们要分配大小除以4 即 16 >>> 4 = 16/16 = 1

  1. 取出Tiny对应的数组
final PoolSubpage<T> head = table[tableIdx];
synchronized (head) {
    final PoolSubpage<T> s = head.next;
    if (s != head) {
        assert s.doNotDestroy && s.elemSize == normCapacity;
        long handle = s.allocate();
        assert handle >= 0;
        s.chunk.initBufWithSubpage(buf, null, handle, reqCapacity);
        incTinySmallAllocation(tiny);
        return;
    }
}

初次分配ByteBuf,创建的PoolSubpage只存在一个节点head,他是一个双向链表的形式,自己指向自己,所以这里取出的节点一定是head节点,所以这里直接返回为false,不会进入到这里!

synchronized (this) {
    //直接分配
    allocateNormal(buf, reqCapacity, normCapacity);
}

开始创建一个新的 PoolSubpage进行内存的分配,我们进入到他的源码逻辑:

private void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
    //大于8k  从q050开始  链表上没有对应的PoolChunk
    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  内涵平衡二叉树
    PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
    //使用 PoolChunk进行分配内存  给ByteBuf
    boolean success = c.allocate(buf, reqCapacity, normCapacity);
    assert success;
    //将 PoolChunk 追加进 qInit队列
    qInit.add(c);
}

基本逻辑和Page分配的逻辑一样,首次分配直接创建一个Chunk对象然后分配内存,我们上面Page的分配中为了简化,我们少讲了一个概念,我们看最后一行:

qInit.add(c);

将创建好的Chunk,使用后加入到使用率队列中:

void add(PoolChunk<T> chunk) {
    //当前的内存使用量大于限制的最大内存的时候后   将该chunk后移
    if (chunk.usage() >= maxUsage) {
        nextList.add(chunk);
        return;
    }
    add0(chunk);
}

这里会判断,当一个Chunk已经达到了一个临界值,Netty会将其放入下一个使用率的队列,例如从q050移动到q075!

我们补充完这个知识点中,继续回到SubPage 的分配逻辑:

io.netty.buffer.PoolArena#allocateNormal

//使用 PoolChunk进行分配内存  给ByteBuf
boolean success = c.allocate(buf, reqCapacity, normCapacity);
boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
    final long handle;
    // >= pageSize   大于等于8k
    if ((normCapacity & subpageOverflowMask) != 0) {
        //返回分配的节点的id   平衡二叉树中的空闲的节点id
        handle =  allocateRun(normCapacity);
    } else {
        //subpage
        handle = allocateSubpage(normCapacity);
    }
    //没有分配成功
    if (handle < 0) {
        return false;
    }
    ByteBuffer nioBuffer = cachedNioBuffers != null ? cachedNioBuffers.pollLast() : null;
    initBuf(buf, nioBuffer, handle, reqCapacity);  //初始化内存
    return true;
}

主要逻辑如下:

  1. 从满二叉树上寻找一个区域,分配一个SunPage

    if ((normCapacity & subpageOverflowMask) != 0)
    

    这里判断分配的内存是否大于8K,明显是不大于的,所以进入到else的逻辑:

    handle = allocateSubpage(normCapacity);
    
    private long allocateSubpage(int normCapacity) {
        //寻找到head节点
        PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity);
        // 子页面只能从页面分配,即离开
        int d = maxOrder;
        synchronized (head) {
            //
            int id = allocateNode(d);
            if (id < 0) {
                return id;
            }
            //记录哪些 Page 被转化为 Subpage
            final PoolSubpage<T>[] subpages = this.subpages;
            final int pageSize = this.pageSize;
    
            freeBytes -= pageSize;
            //计算 pageID对应subpage的Id   2048对应0   2049对应1   2049 ^ 2048 = 1
            int subpageIdx = subpageIdx(id);
            PoolSubpage<T> subpage = subpages[subpageIdx];
            if (subpage == null) {
                // 创建 PoolSubpage,并切分为相同大小的子内存块,然后加入 PoolArena 对应的双向链表中
                subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity);
                subpages[subpageIdx] = subpage;
            } else {
                subpage.init(head, normCapacity);
            }
            return subpage.allocate();
        }
    }
    

    我们进行下逐行分析:

    PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity);
    

    从刚刚讲述的 tinySubpagePools寻找到Head节点

    int d = maxOrder;
    

    我们分配的大小是16B,明显小于8k所以压根不用计算这个大小的节点对应满二叉树的深度,直接定位在最底层 11层,对应的8K节点! d = 11

    int id = allocateNode(d);
    if (id < 0) {
        return id;
    }
    

    和分配Page节点时寻找满二叉树节点的逻辑一样,这里寻找到的是一个8K的节点!不做太多讲解!

    我们试想一下,我们寻找到了一个8K的节点,但是我们分配的仅仅是16B,如果直接使用8K,那么内存空间浪费太过严重,所以我们需要将这个8K的空间进行等分!所以后续逻辑就要切分者8K的空间!

    //记录哪些 Page 被转化为 Subpage
    final PoolSubpage<T>[] subpages = this.subpages;
    

    这里首先获取一个subpages 这也是在构造函数里面初始化的,是一个32长度的数组!

    //8K
    final int pageSize = this.pageSize;
    //计算空闲的空间
    freeBytes -= pageSize;
    

    虽然我们分配了16B,但是也寻找到了8K的空间,所以空闲内存减掉8K!

    //计算 pageID对应subpage的Id   2048对应0   2049对应1   2049 ^ 2048 = 1
    int subpageIdx = subpageIdx(id);
    

    这里会计算满二叉树寻找到的节点对应的subpages的索引信息,比如我们寻找到的第一个8K节点是 2048 ,那么这里通过运算就得到一个0,同样2049的节点就是1,这里就是映射那一块Page要使用SubPage的方式进行分配!

    PoolSubpage<T> subpage = subpages[subpageIdx];
    if (subpage == null) {
        // 创建 PoolSubpage,并切分为相同大小的子内存块,然后加入 PoolArena 对应的双向链表中
        subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity);
        subpages[subpageIdx] = subpage;
    }
    

    第一次取必定为null直接进来:

    // 创建 PoolSubpage,并切分为相同大小的子内存块,然后加入 PoolArena 对应的双向链表中
    subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity);
    

    创建一个 PoolSubpage,我们看他创建PoolSubpage的时候做了什么:

    PoolSubpage(PoolSubpage<T> head, PoolChunk<T> chunk, int memoryMapIdx, int runOffset, int pageSize, int elemSize) {
        this.chunk = chunk;
        this.memoryMapIdx = memoryMapIdx;
        this.runOffset = runOffset;
        this.pageSize = pageSize;
        bitmap = new long[pageSize >>> 10]; // pageSize / 16 / 64
        init(head, elemSize);
    }
    

    除了保存一些变量以外,还调用了初始化方法,我们进入到init方法:

    //初始化PoolSubpage
    void init(PoolSubpage<T> head, int elemSize) {
        doNotDestroy = true;
        this.elemSize = elemSize;
        if (elemSize != 0) {
            //8k / 分配的大小 = 等分为多少份
            maxNumElems = numAvail = pageSize / elemSize;
            nextAvail = 0;
            //除64  long为64位0
            bitmapLength = maxNumElems >>> 6;
            if ((maxNumElems & 63) != 0) {
                bitmapLength ++;
            }
            //初始化 bitmap
            for (int i = 0; i < bitmapLength; i ++) {
                bitmap[i] = 0;
            }
        }
        //将这个page添加到链表
        addToPool(head);
    }
    
    1. 将我们寻找到的8K的节点等分为16B大小的块:

      //8k / 分配的大小 = 等分为多少份
      maxNumElems = numAvail = pageSize / elemSize;
      

      以上代码等价于: 8K / 16B = 512

    2. 填充位图

       //初始化 bitmap
      for (int i = 0; i < bitmapLength; i ++) {
          bitmap[i] = 0;
      }
      

      全部初始化为0,我们现在将一个8K的内存切分为了512个16B的内存块,那么我们以后第二次分配16B的内存的时候,就势必要在这512个内存块里找一个未使用的Subpage,如何记录这个SubPage是否被使用呢?bitmap数组的作用就是记录这个内存块是否被使用的!

    3. 将该SubPage添加到tinySubpagePools的第一个索引位的双向链表中!

      //将这个page添加到链表
      addToPool(head);
      
      private void addToPool(PoolSubpage<T> head) {
          assert prev == null && next == null;
          prev = head;
          next = head.next;
          next.prev = this;
          head.next = this;
      }
      

      一个标准的双向链表的添加逻辑,将该PoolSubPage添加在 tinySubpagePools中的第1个位置上的双向链表上!

  2. 寻找对应内存块的节点id: io.netty.buffer.PoolChunk#allocateSubpage

    subpage.allocate()
    
    long allocate() {
        if (elemSize == 0) {
            return toHandle(0);
        }
    
        if (numAvail == 0 || !doNotDestroy) {
            return -1;
        }
        // 在 bitmap 中找到第一个索引段,然后将该 bit 置为 1
        final int bitmapIdx = getNextAvail();
        // 定位到 bitmap 的数组下标
        int q = bitmapIdx >>> 6;
        // 取到节点对应一个 long 类型中的二进制位
        int r = bitmapIdx & 63;
        assert (bitmap[q] >>> r & 1) == 0;
        bitmap[q] |= 1L << r;
        // 如果 PoolSubpage 没有可分配的内存块,从 PoolArena 双向链表中删除
        if (-- numAvail == 0) {
            removeFromPool();
        }
    
        return toHandle(bitmapIdx);
    }
    

    整个逻辑比较简单,只是里面涉及到各种位运算,导致看起来比较复杂!

    final int bitmapIdx = getNextAvail();
    

    我们之前将一个8K的内存块分为了 512个内存块,这里是寻找未使用的内存块的id!例如:

    • 第一次分配16B内存:返回0

    • 第二次分配16B内存:返回1

    • 第三次分配16B内存:返回2

    • 第四次分配16B内存:返回3

    int q = bitmapIdx >>> 6;
    

    相当于bitmapIdx除以64, 也就是说返回的索引id,当被叠加到64的时候就将索引为后移!

    image-20210517094256391

    int r = bitmapIdx & 63;
    

    确定本次的阶乘数字:

    当分配63次的时候返回63,分配64的时候返回0!

    bitmap[q] |= 1L << r;
    

    给对应的数组赋值,标记为某一个内存已经被使用,如何标记呢?

    这里面的计算逻辑是:

    bitmap[q]=2r1bitmap[q] = 2^r -1

    其中r的变量是第多少次运算,有些同学可能明白了,其实每一位bitMap上的元素的最大值是long的最大值,因为long的最大值就是:

    LONG_MAX=2631LONG\_MAX = 2^{63} -1

    当元素达到了long的最大值,那么久将数组元素后移!

    if (-- numAvail == 0) {
        removeFromPool();
    }
    

    numAvail:这个是记录了我们将一个Page平分了多少个,我们这里例子平分了512份!每次分配成功一个 就减一,如果剩余的可用的块为0的话,就删除这个PoolSubPage!

    toHandle(bitmapIdx);
    
    private long toHandle(int bitmapIdx) {
        return 0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx;
    }
    

    以0x4000000000000000L为高32位,以 (long) bitmapIdx << 32 | memoryMapIdx;为第32位!计算出来一个值,以便于后续能够区分Page分配还是 Subpage分配!

  3. 开始准备初始化内存:io.netty.buffer.PoolChunk#allocate:

    //初始化内存
    initBuf(buf, nioBuffer, handle, reqCapacity);  
    
    void initBuf(PooledByteBuf<T> buf, ByteBuffer nioBuffer, long handle, int reqCapacity) {
        //再平衡二叉树上找的节点id
        int memoryMapIdx = memoryMapIdx(handle);
        int bitmapIdx = bitmapIdx(handle);
        if (bitmapIdx == 0) {
            //查询是否为不可用  不可用就对了
            byte val = value(memoryMapIdx);
            assert val == unusable : String.valueOf(val);
            buf.init(this, nioBuffer, handle, runOffset(memoryMapIdx) + offset,
                     reqCapacity, runLength(memoryMapIdx), arena.parent.threadCache());
        } else {
            initBufWithSubpage(buf, nioBuffer, handle, bitmapIdx, reqCapacity);
        }
    }
    
    • 计算对应满二叉树上的节点的id

    • int memoryMapIdx = memoryMapIdx(handle);
      
    • 这里一定返回的是2048

    • int bitmapIdx = bitmapIdx(handle);
      
    • 转换为一个映射的id, 这里注意,上述的高32位和第32位的计算就是为区分这个,当为SubPage分配的时候,这里返回的值一定不会为0,所以进入到else分支:

    • initBufWithSubpage(buf, nioBuffer, handle, bitmapIdx, reqCapacity);
      
    • private void initBufWithSubpage(PooledByteBuf<T> buf, ByteBuffer nioBuffer,
                                          long handle, int bitmapIdx, int reqCapacity) {
             ..............................................................
      
              buf.init(
                  this, nioBuffer, handle,
                  runOffset(memoryMapIdx) + (bitmapIdx & 0x3FFFFFFF) * subpage.elemSize + offset,
                      reqCapacity, subpage.elemSize, arena.parent.threadCache());
          }
      
    • 这里面比较重要的一点就是计算偏移量:

    • runOffset(memoryMapIdx) + (bitmapIdx & 0x3FFFFFFF) * subpage.elemSize + offset

    • 这里注意,我们的SubPage是在Page内部进行分配的,所以首先我们要先计算Page的偏移量即:runOffset(memoryMapIdx) 这里一定是为0的,因为我们寻找到的就是第一个8K的page块!

    • 其次是计算SubPage再Page里面的偏移量,(bitmapIdx & 0x3FFFFFFF) * subpage.elemSize! 其中(bitmapIdx & 0x3FFFFFFF)简单理解为分配的次数 elemSize理解为分配的内存大小,我们首次分配即为 0 * 16

    • 首次分配16B的内存,该偏移量的计算为:0+0+0 = 0

    • 第二次分配16B的内存为:0+16+0

    • 第三次分配32B的内存:8192 + 0 + 0

    • 为什么是8192,因为是重新分配了一块8K的内存,原先的8KPage内存已经被平分为了16B大小的内存块,无法分配32B的,所以需要重新寻找一个8K的Page进行平分!

    buf.init(
                this, nioBuffer, handle,
                runOffset(memoryMapIdx) + (bitmapIdx & 0x3FFFFFFF) * subpage.elemSize + offset,
                    reqCapacity, subpage.elemSize, arena.parent.threadCache());
    

    改行代码与之前Page分配的逻辑相同,不做讲解!

3. 内存的释放

byteBuf.release();

io.netty.buffer.AbstractReferenceCountedByteBuf#release()

@Override
public boolean release() {
    return handleRelease(updater.release(this));
}

private boolean handleRelease(boolean result) {
    if (result) {
        deallocate();
    }
    return result;
}

io.netty.buffer.PooledByteBuf#deallocate

@Override
protected final void deallocate() {
    if (handle >= 0) {
        final long handle = this.handle;
        this.handle = -1;
        memory = null;
        chunk.arena.free(chunk, tmpNioBuf, handle, maxLength, cache);
        tmpNioBuf = null;
        chunk = null;
        recycle();
    }
}

这里我们总结为三个步骤:

  1. 添加到线程缓存中

    chunk.arena.free(chunk, tmpNioBuf, handle, maxLength, cache);
    
    void free(PoolChunk<T> chunk, ByteBuffer nioBuffer, long handle, int normCapacity, PoolThreadCache cache) {
        if (chunk.unpooled) {
            int size = chunk.chunkSize();
            destroyChunk(chunk);
            activeBytesHuge.add(-size);
            deallocationsHuge.increment();
        } else {
            SizeClass sizeClass = sizeClass(normCapacity);
            if (cache != null && cache.add(this, chunk, nioBuffer, handle, normCapacity, sizeClass)) {
                // 缓存,所以不能释放它。
                return;
            }
    
            freeChunk(chunk, handle, sizeClass, nioBuffer, false);
        }
    }
    

    会首先判断当前的类型是不是Pool类型的,当然是,所以走到else分支:

    SizeClass sizeClass = sizeClass(normCapacity);
    if (cache != null && cache.add(this, chunk, nioBuffer, handle, normCapacity, sizeClass)) {
        // 缓存,所以不能释放它。
        return;
    }
    
    freeChunk(chunk, handle, sizeClass, nioBuffer, false);
    

    这里分为两个过程:

    1. 加入到缓冲池

      cache != null && cache.add(this, chunk, nioBuffer, handle, normCapacity, sizeClass)
      

      我们前面分析分配内存的时候,有一段逻辑是,先判断线程缓冲池里面有没有,有的话直接返回没有的话就进行分配,那么线程缓冲池就是现在被加入的!

      boolean add(PoolArena<?> area, PoolChunk chunk, ByteBuffer nioBuffer,
                  long handle, int normCapacity, SizeClass sizeClass) {
          MemoryRegionCache<?> cache = cache(area, normCapacity, sizeClass);
          if (cache == null) {
              return false;
          }
          return cache.add(chunk, nioBuffer, handle);
      }
      

      image-20210517114345669

      对应的线程缓冲池里面定义了六个数组,其中tinySubPageDirectCaches类型的长度为32! 这里会将释放内存的大小/16计算当前释放的内存属于那个 MemoryRegionCache!

      寻找到对应的MemoryRegionCache后,将该缓冲池加入到MemoryRegionCache的队列中,以便后续使用!

    2. 加入缓冲池失败就释放连续的内存

      freeChunk(chunk, handle, sizeClass, nioBuffer, false);
      
      void freeChunk(PoolChunk<T> chunk, long handle, SizeClass sizeClass, ByteBuffer nioBuffer, boolean finalizer) {
              final boolean destroyChunk;
              synchronized (this) {
                  if (!finalizer) {
                      switch (sizeClass) {
                          case Normal:
                              ++deallocationsNormal;
                              break;
                          case Small:
                              ++deallocationsSmall;
                              break;
                          case Tiny:
                              ++deallocationsTiny;
                              break;
                          default:
                              throw new Error();
                      }
                  }
                  destroyChunk = !chunk.parent.free(chunk, handle, nioBuffer);
              }
              if (destroyChunk) {
                  // 保持同步锁时不需要调用destroyChunk。
                  destroyChunk(chunk);
              }
          }
      
      destroyChunk = !chunk.parent.free(chunk, handle, nioBuffer);
      
      boolean free(PoolChunk<T> chunk, long handle, ByteBuffer nioBuffer) {
          chunk.free(handle, nioBuffer);
          if (chunk.usage() < minUsage) {
              remove(chunk);
              // Move the PoolChunk down the PoolChunkList linked-list.
              return move0(chunk);
          }
          return true;
      }
      
      • 标记一段连续的内存为未使用!

        chunk.free(handle, nioBuffer);
        
        /**
         * 释放一个子页面或页面的一部分当从PoolSubpage中释放一个子页面时,可能会将其添加回拥有的PoolArena的子页面池中。
         * 如果PoolArena中的子页面池还具有至少另一个给定elemSize的PoolSubpage,我们可以完全释放其拥有者页面,因此可用于后*
         * 续分配
         *
         * @param handle handle to free
         */
            void free(long handle, ByteBuffer nioBuffer) {
                int memoryMapIdx = memoryMapIdx(handle);
                int bitmapIdx = bitmapIdx(handle);
                //当时subpage的时候
                if (bitmapIdx != 0) {
                    //获取当前对应的PoolSubpage
                    PoolSubpage<T> subpage = subpages[subpageIdx(memoryMapIdx)];
                    assert subpage != null && subpage.doNotDestroy;
                    //寻找PoolSubpage的head节点
                    PoolSubpage<T> head = arena.findSubpagePoolHead(subpage.elemSize);
                    synchronized (head) {
                        //与分配内存反向操作 包括减少bitmap的值和增加可用内存块的数量
                        if (subpage.free(head, bitmapIdx & 0x3FFFFFFF)) {
                            return;
                        }
                    }
                }
                //Page级别的内存分配
                //空闲内存 + 当前要释放的内存
                freeBytes += runLength(memoryMapIdx);
                //设置满二叉树的状态为未使用
                setValue(memoryMapIdx, depth(memoryMapIdx));
                //迭代更新父级节点为未使用
                updateParentsFree(memoryMapIdx);
        
                if (nioBuffer != null && cachedNioBuffers != null &&
                        cachedNioBuffers.size() < PooledByteBufAllocator.DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK) {
                    cachedNioBuffers.offer(nioBuffer);
                }
            }
        
      • 释放这一段内存,将Chunk对应的16M内存块,移动到对应的使用率的队列里面!

        if (chunk.usage() < minUsage) {
            remove(chunk);
            // 如果当前的Chunk小于对应队列使用率的临界值,就将该CHunk删除并向前一个队列移动!
            return move0(chunk);
        }
        
  2. 加入到对象缓存池

    回到:io.netty.buffer.PooledByteBuf#deallocate

     recycle();
    
    private void recycle() {
            recyclerHandle.recycle(this);
        }
    

    将当前这个对象压入自己的栈中!防止GC,后续使用的时候直接返回!

四、总结

  1. 分配内存分为三种模式的分配,Page、SubPage、Huge
  2. Page的默认大小是8K,当我们分配的大小为大于等于8K的时候,会寻找一个或者多个连续的Page进行返回和分配!
  3. SubPage专注于分配小于8K的内存,他会寻找一个对应的Page, 将其等分为相等的若干份内存块,然后返回使用!
  4. Huge分配的是大于16M的不做任何优化操作,直接分配!
  5. 释放内存的时候,会将该段内存添加到线程缓存内使用,若下次重复获取数据,则直接从当前的线程缓冲中获取,避免重复分配内存耗时!
  6. 当加入到县城缓存失败的时候,会将该段内存标记为释放,增加空闲内存!
  7. 当程序qps过高的时候,为了防止重复的创建ByteBuf,造成GC的压力,所以将ByteBuf缓存起来,重复使用,避免反复创建!