这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战
Netty 作为一款高性能的网络框架,需要处理海量的字节数据,而且 Netty 默认提供了池化对象的内存分配,使用完后归还到内存池,所以一套高性能的内存管理机制是 Netty 必不可少的。Netty是如何对内存进行优化的呢?
一、内存的种类
Netty中的内存类型总的可以归为两大类,六小类!我们查看他的类图:
我们可以看到,这里的内存的种类是非常多的,不同的种类有八种之多,我们通过三个维度对他进行介绍:
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有两个 heapBuffer 、 directBuffer 这里体现了两个维度: 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);
}
我么可以看到两个实现类最终调用的方法:newHeapBuffer、newDirectBuffer, 都是抽象方法,具体的实现交给子类来实现,我们查看以下他的子类有哪些:
至此,又一个维度被我们分析出来了,判断一个类是否需要池化,需要依赖于子类的实现,我们现在为止分析出来了两个维度: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);
}
.....................................................................
}
其实我们乍一看这个逻辑不咋复杂,我们暂时不考虑他具体是如何实现的,我们单看这个方法,主线逻辑:
- 从一个类似缓存的东西里面取出了一个
PoolArena - 然后使用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;
}
这里面分为两个步骤:
-
创建或者获取一个PooledByteBuf
@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(); } -
给获取出来的PooledByteBuf分配内存
allocate(cache, buf, reqCapacity);开始分配内存,在Netty中,Netty对于不同大小的内存分配,设定了不同的分配方式,将Netty的内存规格介绍一下:
Netty对于分配16B
496B之间的内存称之为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的满二叉树:
例如:当我们分配一个 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类型的内存!
-
先尝试从当前的线程中取出一块合适的内存,我们现在是第一次分配,这里当然会返回false
-
开始进行实际的内存分配!
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,他们的内存结如下:
每一个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,我们查看创建的时候做了什么!
-
@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
-
-
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个长度!这里的代码的意思是构建出来了一个平衡满二叉树:
-
该图 对应上图的满二叉树即从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;
}
这里分为三个步骤:
-
计算当前这个分配的大小,对应满二叉树的深度!
int d = maxOrder - (log2(normCapacity) - pageShifts);上述的表达式对应: 11 - (log2(16384) - 13) = 10, 最底层的叶子节点为8K,其父级一定是16K.也就是第十层!
-
在对应深度上寻找一个对应大小的节点,返回他的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,然后遍历父节点,逐层向上修改!父节点的值等于子节点的最小值,如下图所示!
这样就寻找到对应的节点了,对应的节点的Id为 1024!
当我们再次分配一个16K 的节点的时候,还是寻找到10,取出1024的值为12,大于10,所以使用
id ^= 1;得到1025,取出1025的值为10,不大于10,所以返回1025,同时修改本节点为12,再次逐层向上修改: -
减少空闲内存的余量
//空闲内存-刚刚分配的内存 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):
第二次分配(分配规格16K):
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();
}
这里总共分为两个步骤:
-
初始化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的初始化就是对各种值进行赋值,完成初始化!
-
-
初始化地址,因为是基于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;
-
尝试从线程缓存中获取一个合适的ByteBuf,我们第一次分配该大小的内存,所以缓存没有,就返回false
-
算出当前的空间大小的内存属于第几个位置,:
我们之前说到过,我们对于Tiny的分配如下图所示:
即32个内存种类,Netty的PoolArena中维护了两个数组,分别是:
private final PoolSubpage<T>[] tinySubpagePools; //32
private final PoolSubpage<T>[] smallSubpagePools; //4
tinySubpagePools的内存结构如下所示:
他在构造函数中初始化,这里的tinyIdx方法就是计算我们当前所分配的大小属于在内存块的哪一个地方,我们进入到 tinyIdx(normCapacity);
static int tinyIdx(int normCapacity) {
return normCapacity >>> 4;
}
逻辑很简单就是直接拿我们要分配大小除以4 即 16 >>> 4 = 16/16 = 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;
}
主要逻辑如下:
-
从满二叉树上寻找一个区域,分配一个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); }-
将我们寻找到的8K的节点等分为16B大小的块:
//8k / 分配的大小 = 等分为多少份 maxNumElems = numAvail = pageSize / elemSize;以上代码等价于: 8K / 16B = 512
-
填充位图
//初始化 bitmap for (int i = 0; i < bitmapLength; i ++) { bitmap[i] = 0; }全部初始化为0,我们现在将一个8K的内存切分为了512个16B的内存块,那么我们以后第二次分配16B的内存的时候,就势必要在这512个内存块里找一个未使用的Subpage,如何记录这个SubPage是否被使用呢?bitmap数组的作用就是记录这个内存块是否被使用的!
-
将该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个位置上的双向链表上!
-
-
寻找对应内存块的节点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的时候就将索引为后移!
int r = bitmapIdx & 63;确定本次的阶乘数字:
当分配63次的时候返回63,分配64的时候返回0!
bitmap[q] |= 1L << r;给对应的数组赋值,标记为某一个内存已经被使用,如何标记呢?
这里面的计算逻辑是:
其中r的变量是第多少次运算,有些同学可能明白了,其实每一位bitMap上的元素的最大值是long的最大值,因为long的最大值就是:
当元素达到了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分配!
-
-
开始准备初始化内存: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();
}
}
这里我们总结为三个步骤:
-
添加到线程缓存中
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);这里分为两个过程:
-
加入到缓冲池
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); }对应的线程缓冲池里面定义了六个数组,其中tinySubPageDirectCaches类型的长度为32! 这里会将释放内存的大小/16计算当前释放的内存属于那个 MemoryRegionCache!
寻找到对应的MemoryRegionCache后,将该缓冲池加入到MemoryRegionCache的队列中,以便后续使用!
-
加入缓冲池失败就释放连续的内存
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); }
-
-
-
加入到对象缓存池
回到:io.netty.buffer.PooledByteBuf#deallocate
recycle();private void recycle() { recyclerHandle.recycle(this); }将当前这个对象压入自己的栈中!防止GC,后续使用的时候直接返回!
四、总结
- 分配内存分为三种模式的分配,Page、SubPage、Huge
- Page的默认大小是8K,当我们分配的大小为大于等于8K的时候,会寻找一个或者多个连续的Page进行返回和分配!
- SubPage专注于分配小于8K的内存,他会寻找一个对应的Page, 将其等分为相等的若干份内存块,然后返回使用!
- Huge分配的是大于16M的不做任何优化操作,直接分配!
- 释放内存的时候,会将该段内存添加到线程缓存内使用,若下次重复获取数据,则直接从当前的线程缓冲中获取,避免重复分配内存耗时!
- 当加入到县城缓存失败的时候,会将该段内存标记为释放,增加空闲内存!
- 当程序qps过高的时候,为了防止重复的创建ByteBuf,造成GC的压力,所以将ByteBuf缓存起来,重复使用,避免反复创建!