昨天,我彻底搞懂了Netty内存分配策略( 五 )

所有内存分配的 size 都会经过 normalizeCapacity() 进行处理,申请的容量总是会被格式为 2^N 。
主要规则如下:

  • 如果目标容量小于 16 字节,则返回 16 。
  • 如果目标容量大于 16 字节,小于 512 字节,则以 16 字节为单位,返回大于目标字节数的第一个 16 字节的倍数 。比如申请的 100 字节,那么大于 100 的 16 整数倍最低为:16*7=112,因而返回 112 。
  • 如果目标容量大于 512 字节,则返回大于目标容量的第一个 2 的指数幂 。比如申请的 1000 字节,那么返回的将是:2^10 = 1024 。
PoolArena 提供了两种方式进行内存分配:
①PoolSubpage 用于分配小于 8k 的内存
tinySubpagePools:用于分配小于 512 字节的内存,默认长度为 32,因为内存分配最小为 16,每次增加 16,直到 512,区间 [16,512) 一共有 32 个不同值 。
smallSubpagePools:用于分配大于等于 512 字节的内存,默认长度为 4 。tinySubpagePools 和 smallSubpagePools 中的元素默认都是 subpage 。
②poolChunkList 用于分配大于 8k 的内存
上面已经解释了 q 开头的几个变量用于保存大于 8k 的数据 。
默认先尝试从 poolThreadCache 中分配内存,PoolThreadCache 利用 ThreadLocal 的特性,消除了多线程竞争,提高内存分配效率;
首次分配时,poolThreadCache 中并没有可用内存进行分配,当上一次分配的内存使用完并释放时,会将其加入到 poolThreadCache 中,提供该线程下次申请时使用 。
如果是分配小内存,则尝试从 tinySubpagePools 或 smallSubpagePools 中分配内存,如果没有合适 subpage,则采用方法 allocateNormal 分配内存 。
如果分配一个 page 以上的内存,直接采用方法 allocateNormal() 分配内存,allocateNormal() 则会将申请动作交由 PoolChunkList 进行 。
private synchronized void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {  //如果在对应的PoolChunkList能申请到内存,则返回  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)) {    ++allocationsNormal;    return;  }  // Add a new chunk.  PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);  long handle = c.allocate(normCapacity);  ++allocationsNormal;  assert handle > 0;  c.initBuf(buf, handle, reqCapacity);  qInit.add(c);}首先将申请动作按照 q050→q025→q000→qInit→q075 的顺序依次交由各个 PoolChunkList 进行处理,如果在对应的 PoolChunkList 中申请到了内存,则直接返回 。
如果申请不到,那么直接创建一个新的 PoolChunk,然后在该 PoolChunk 中申请目标内存,最后将该 PoolChunk 添加到 qInit 中 。
上面说过 Chunk 是 Netty 向操作系统申请内存块的最大单位,每个 Chunk 是 16M 。
PoolChunk 内部通过 memoryMap 数组维护了一颗完全平衡二叉树作为管理底层内存分布及回收的标记位,所有的子节点管理的内存也属于其父节点 。
关于 PoolChunk 内部如何维护完全平衡二叉树就不在这里展开,大家有兴趣可以自行看源码 。
对于内存的释放,PoolArena 主要是分为两种情况,即池化和非池化,如果是非池化,则会直接销毁目标内存块,如果是池化的,则会将其添加到当前线程的缓存中 。
如下是 free() 方法的源码:
public 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 {    // 如果是池化的,首先判断其是哪种类型的,即tiny,small或者normal,    // 然后将其交由当前线程的缓存进行处理,如果添加成功,则直接返回    SizeClass sizeClass = sizeClass(normCapacity);    if (cache != null && cache.add(this, chunk, nioBuffer, handle,          normCapacity, sizeClass)) {      return;    }    // 如果当前线程的缓存已满,则将目标内存块返还给公共内存块进行处理    freeChunk(chunk, handle, sizeClass, nioBuffer);  }}


推荐阅读