随笔分类
尝试三之 Arena中 PoolChunkList进行分配
接着往下看:
// 锁住当前 Arena
synchronized (this) {
// 最为复杂的逻辑... Arena的 subpagePools和线程的本地缓存 PoolThreadCache都没能满足内存的分配, 则会走 allocateNormal逻辑
// 参数一:上一步获取的一个内存容器, 下面逻辑会给 buf去分配真正的内存
// 参数二:指定想要去分配的内存容量
// 参数三:规格化后的 size
allocateNormal(buf, reqCapacity, normCapacity);
}
锁住了当前 Arena,然后去调用了 allocateNormal()方法
// 最为复杂的逻辑... Arena的 subpagePools和线程的本地缓存 PoolThreadCache都没能满足内存的分配, 则会走 allocateNormal逻辑
// 参数一:上一步获取的一个内存容器, 下面逻辑会给 buf去分配真正的内存 - ByteBuf类型
// 参数二:指定想要去分配的内存容量
// 参数三:规格化后的 size
// Method must be called inside synchronized(this) { ... } block
private void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
// 程序会先尝试到 PoolChunkList中去分配内存, 这里第一次时会失败, 因为 chunkList中还未添加任何的 chunk
// 从源码分析角度, 咋们先来分析最为复杂的场景 - 程序未能从 chunkList中分配内存成功
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;
}
// 执行到这, 说明在 PoolChunkList中申请内存未成功, 需要创建一个新的 chunk, 在 chunk中申请内存
// Add a new chunk.
// 创建出一个新的 chunk(16mb, 使用满二叉树表示其内部占用情况, 并且拥有一个长度为 2048的 Subpages数组)
// 参数一:8k
// 参数二:11
// 参数三:13
// 参数四:16mb
PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
// 参数一:buf, 返回给业务属于的 ByteBuf对象, 下面逻辑会给该 buf分配真正的内存
// 参数二:指定想要去分配的内存容量
// 参数三:规格化后的 size
boolean success = c.allocate(buf, reqCapacity, normCapacity);
assert success;
qInit.add(c);
}
PoolChunkList.allocate()
boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
if (normCapacity > maxCapacity) {
// Either this PoolChunkList is empty or the requested capacity is larger then the capacity which can
// be handled by the PoolChunks that are contained in this PoolChunkList.
return false;
}
// 遍历 PoolChunkList中的 chunk
for (PoolChunk<T> cur = head; cur != null; cur = cur.next) {
// 尝试从当前迭代到 chunk中去进行内存的分配
if (cur.allocate(buf, reqCapacity, normCapacity)) {
// 条件成立, 说明当前 chunk中的内存使用率已经达到当前 chunkList中规定的上限了
if (cur.usage() >= maxUsage) {
// 从当前 chunkList中移除当前迭代的 chunk
remove(cur);
// 将当前迭代的 chunk添加到下一个 chunkList中去, 对应的便是更大的一个内存使用率的上线:qInit -> q0, q0 -> q25
nextList.add(cur);
}
return true;
}
}
return false;
}
可以看到,这里尝试到当前 Arena中的 PoolChunkList中去申请内存,而这块初始时 chunkList还并没存储着任何的 chunk,而我们假设是程序第一次执行到这,因此,这块也会失败
既然没有对应的 chunk分配,那就去创建 chunk呗,可以看到程序去创建了 Chunk
尝试四之创建 PoolChunk
我们考虑的是 directArena中 PoolChunk的创建,对应:
// 创建出一个新的 chunk
// 参数一:8k
// 参数二:11
// 参数三:13
// 参数四:16mb
@Override
protected PoolChunk<ByteBuffer> newChunk(int pageSize, int maxOrder,
int pageShifts, int chunkSize) {
// 默认情况下, 这里会是 true - 直接内存对齐基准为 0
if (directMemoryCacheAlignment == 0) {
// 参数一:当前 directArena对象, 创建出来的 chunk需要知道其归属的 "爸爸"是谁
// 参数二:allocateDirect(chunkSize) 非常重要, 方法内部会通过 Unsafe方式完成 directByteBuffer内存的申请;
// 申请多少?返回 16mb给我们 - 从这里也可以看出, 内存实际上是由 JDK层面的 Bytebuffer来进行管理的
// 参数三:8k
// 参数四:11
// 参数五:13
// 参数六:16mb
// 参数七:offset - 0
return new PoolChunk<ByteBuffer>(this,
allocateDirect(chunkSize), pageSize, maxOrder,
pageShifts, chunkSize, 0);
}
// 不会走到下边
final ByteBuffer memory = allocateDirect(chunkSize
+ directMemoryCacheAlignment);
return new PoolChunk<ByteBuffer>(this, memory, pageSize,
maxOrder, pageShifts, chunkSize,
offsetCacheLine(memory));
}
参数中有去调用了方法:allocateDirect(chunkSize)
这里简单看下就行,发现这块会通过 Unsafe的方式完成 directByteBuffer内存的申请,申请多少? 16mb
从这块也可以看出,内存实际上是由 JDK层面的 ByteBuffer进行管理的
private static ByteBuffer allocateDirect(int capacity) {
return PlatformDependent.useDirectBufferNoCleaner() ?
PlatformDependent.allocateDirectNoCleaner(capacity) : ByteBuffer.allocateDirect(capacity);
}
static ByteBuffer allocateDirectNoCleaner(int capacity) {
// Calling malloc with capacity of 0 may return a null ptr or a memory address that can be used.
// Just use 1 to make it safe to use in all cases:
// See: http://pubs.opengroup.org/onlinepubs/009695399/functions/malloc.html
return newDirectBuffer(UNSAFE.allocateMemory(Math.max(1, capacity)), capacity);
}
// 参数一:当前 directArena对象, 创建出来的 chunk需要知道其归属的 "爸爸"是谁
// 参数二:allocateDirect(chunkSize) 非常重要, 方法内部会通过 Unsafe方式完成 directByteBuffer内存的申请;
// 申请多少?返回 16mb给我们 - 从这里也可以看出, 内存实际上是由 JDK层面的 Bytebuffer来进行管理的
// 参数三:8k
// 参数四:11
// 参数五:13
// 参数六:16mb
// 参数七:offset - 0
PoolChunk(PoolArena<T> arena, T memory, int pageSize, int maxOrder, int pageShifts, int chunkSize, int offset) {
// 表示使用池化内存
unpooled = false;
this.arena = arena;
// 让 chunk去持有 byteBuffer对象
this.memory = memory;
this.pageSize = pageSize;
this.pageShifts = pageShifts;
this.maxOrder = maxOrder;
this.chunkSize = chunkSize;
this.offset = offset;
// chunk内使用一颗满二叉树表示内存的占用情况, 二叉树中的每个节点有三个维度的数据(深度, 节点 id, 可分配深度能力值 - 初始值和深度一致)
// unusable表示当某个节点上的内存被分配出去时, 它的可分配深度值就需要改为 unusable - 即, 以后就不能再去使用这个节点进行内存分配了
// 12
unusable = (byte) (maxOrder + 1);
// 24
log2ChunkSize = log2(chunkSize);
// (pageSize - 1) -> 0b 0000 0000 0000 0000 0001 1111 1111 1111
// 0b 1111 1111 1111 1111 1110 0000 0000 0000 0000
// 这个数有什么用 ? - 当我们申请的内存大于 page时, 申请的容量会与 subpageOverflowMask进行位与运算, 得出一个非零值
subpageOverflowMask = ~(pageSize - 1);
// 16mb, 当前 chunk中可分配的字节数
freeBytes = chunkSize;
assert maxOrder < 30 : "maxOrder should be < 30, but is: " + maxOrder;
// 最多可申请 subpage数目 - 1 << 11 - 2046个子页(对应的便是满二叉树 11层叶子节点数目)
maxSubpageAllocs = 1 << maxOrder;
// Generate the memory map.
// 可以看到, 这里并没有去使用 Node来去表示满二叉树, 而是以数组的形式去表示满二叉树
// i的左子节点:i * 2; i的右子节点:i * 2 + 1
// 创建了长度为 4096的数组, 以数组的形式去表示满二叉树中每个节点对应的可分配深度能力值, 初始值与节点对应的深度值(0 ~ 11)一致
// 如:memoryMap[1] == 0 - 表示根节点可去分配整个内存
// memoryMap[2] == 1 - 表示当前节点可去管理 chunk的一半内存
// 节点对应的可分配深度能力值是会发生改变的 - 当节点所管理的内存被分配出去时
memoryMap = new byte[maxSubpageAllocs << 1];
// 以数组形式表示满二叉树中每个节点对应的深度值
// 节点对应的深度值是固定的, 不会发生改变
depthMap = new byte[memoryMap.length];
int memoryMapIndex = 1;
// 接下来这个算法其实就是去将 memoryMap、depthMap初始化下而已, 对应 [0, 1, 1, 2, 2, 2, 2, 3, 3, ....]
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 ++;
}
}
// 默认 chunk下, 可以去开辟 2048个 Subpage子页, 所以这里就去创建了一个长度为 2048的 Subpage数组
subpages = newSubpageArray(maxSubpageAllocs);
cachedNioBuffers = new ArrayDeque<ByteBuffer>(8);
}
从这,我们可以看出,chunk里头管理着 16mb的内存,这块将其存进了字段 memory中
接着,这块内存的使用情况如何进行表示呢?
chunk里头使用一颗满二叉树去表示其管理的内存的占用情况,默认深度 11 (0 ~ 11)
对于每个节点,其实对应着三个维度的数据:节点 id,节点所在深度,节点深度可分配能力值
对应前两者,是固定值,且根据节点 id,我们可以计算出节点所在深度
而对于后者,其值是不固定的,随着节点管理的内存变化而变化
这块并没有去使用传统 Node方式去表示二叉树,而是使用了两个数组来去表示 chunk中的满二叉树
对应:memoryMap、depthMap,前者表示的是节点对应的可分配深度能力值,其值越小,代表着当前节点可能的内存也就越大,后者对应的便是节点在二叉树中对应的深度,是固定值
接着,便是去初始化 memoryMap、depthMap,每个节点可分配深度能力值在初始化时其实和节点所在深度是一致的
接着,便是去创建了长度为 2048的 PoolSubpage数组,为什么是 2048呢?
其实 chunk中满二叉树中叶子节点才是真意义上的内存,对应的便是 2048个叶子节点,而 chunk中管理的是 16mb内存,so,每个叶子节点对应大小 8kb,这也正对应了 pageSize
so,这里来创建了长度为 2048的 PoolSubpage数组,对应的便是叶子节点,其实就是通过 PoolSubpage来对 chunk所管理的内存进行精细粒度的管理
这块,chunk便创建出来了,接着继续回到主逻辑:PoolArena.allocateNormal()
// 最为复杂的逻辑... Arena的 subpagePools和线程的本地缓存 PoolThreadCache都没能满足内存的分配, 则会走 allocateNormal逻辑
// 参数一:上一步获取的一个内存容器, 下面逻辑会给 buf去分配真正的内存 - ByteBuf类型
// 参数二:指定想要去分配的内存容量
// 参数三:规格化后的 size
// Method must be called inside synchronized(this) { ... } block
private void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
// 程序会先尝试到 PoolChunkList中去分配内存, 这里第一次时会失败, 因为 chunkList中还未添加任何的 chunk
// 从源码分析角度, 咋们先来分析最为复杂的场景 - 程序未能从 chunkList中分配内存成功
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;
}
// 执行到这, 说明在 PoolChunkList中申请内存未成功, 需要创建一个新的 chunk, 在 chunk中申请内存
// Add a new chunk.
// 创建出一个新的 chunk(16mb, 使用满二叉树表示其内部占用情况, 并且拥有一个长度为 2048的 Subpages数组)
// 参数一:8k
// 参数二:11
// 参数三:13
// 参数四:16mb
PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
// 参数一:buf, 返回给业务属于的 ByteBuf对象, 下面逻辑会给该 buf分配真正的内存
// 参数二:指定想要去分配的内存容量
// 参数三:规格化后的 size
boolean success = c.allocate(buf, reqCapacity, normCapacity);
assert success;
qInit.add(c);
}
可以看到,接下来便是通过刚创建出来的 PoolChunk去进行内存的分配:c.allocate()
// 参数一:buf, 返回给业务属于的 ByteBuf对象, 下面逻辑会给该 buf分配真正的内存
// 参数二:指定想要去分配的内存容量
// 参数三:规格化后的 size
// 注:16mb以及以内的内存能够到 chunk上进行分配
// 从 chunk中申请内存正对应着两种逻辑:>= 8k, < 8k
boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
// 这是一个非常重要的值
final long handle;
// true - 规格化后的 size >= 8kb - 业务需求量大于等于 pageSize(即 8kb, 16kb, 32kb...)
if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize
// Normal类型规格内存的申请(小于等于 16mb)
// 这里的 handler是当前分配节点占用的 树节点对应的 id, 可能是 -1, -1表示申请失败
handle = allocateRun(normCapacity);
} else { // 业务需求量小于 pageSize(tiny、small规格类型)
// 说明业务需要的内存是小规格的内存:tiny、small
// 这里便是去申请一夜内存的逻辑
// 这里的 handler是当前分配节点占用的 树节点对应的 id, 可能是 -1, -1表示申请失败
handle = allocateSubpage(normCapacity);
}
// 执行到这里, 会拿到一个 handle值, 该值可能是 allocateRun() 或 allocateSubpage()返回值
// 条件成立, 说明内存申请分配失败 -1
if (handle < 0) {
return false;
}
// 暂不考虑了, 这里假设获取的是 null
ByteBuffer nioBuffer = cachedNioBuffers != null ? cachedNioBuffers.pollLast() : null;
// 参数一:byteBuf, 返回给上层业务的 ByteBuf对象, 需要给该 buf分配真正的内存
// 参数二:bull
// 参数三:allocateRun()或 allocateSubpage()返回的 handle
// 参数四:指定想要去分配的内存容量
// 核心方法
initBuf(buf, nioBuffer, handle, reqCapacity);
// 来到这, 已经为 buf设置好内存信息了, 上层业务可以使用了
return true;
}
在 chunk中进行内存的分配,其实对应着两条主线逻辑:size >= 8k,size < 8k
前者对应的是 allocateRun()方法
allocateRun()
/**
* Allocate a run of pages (>=1)
*
* @param normCapacity normalized capacity
* @return index in memoryMap
*/
// 参数:规格化后的 size
// Normal类型规格会来到这里
private long allocateRun(int normCapacity) {
// 这里来计算出一个节点深度值, 表示 normCapacity内存量的申请需要到深度为 d的节点上去分配内存
// 假设需要分配内存量是 16k的话, 这里计算出 10, 表示需要到深度值为 10的节点上去进行内存的分配
int d = maxOrder - (log2(normCapacity) - pageShifts);
// 到满二叉树上去分配内存
// 参数:内存容量大小对应的节点深度值
// 返回值 id - 满二叉树中对应内存分配的节点 id
int id = allocateNode(d);
// true - 说明内存分配失败, 此时 id == -1
if (id < 0) {
return id;
}
freeBytes -= runLength(id);
return id;
}
这块,就是先去计算出一个深度能力值 d,表示的便是规格化的 size内存量的申请需要到深度为 d的节点上去进行内存的分配,对应的便是方法:allocateNode(d) -> 这块是 Normal规格类型的 size的分配,因此在二叉树上肯定会找到一个合适的节点,如果当前二叉树还有容量进行分配的话
allocateNode()
/**
* Algorithm to allocate an index in memoryMap when we query for a free node
* at depth d
*
* @param d depth
* @return index in memoryMap
*/
// 到满二叉树上去分配内存
// 参数:内存容量大小对应的节点深度值 - 为便于理解, 这里假设是 11的情况, 对应 8k
private int allocateNode(int d) {
// id is 1 表示的节点是根节点, allocateNode方法会从根节点开始往下搜索, 到深度为 d的上去占用节点
int id = 1;
// (1 << 11) - 0b 0000 0000 0000 0000 0000 1000 0000 0000
// -(1 << 11) - 0b 1111 1111 1111 1111 1111 1000 0000 0000
int initial = - (1 << d); // has last d bits = 0 and rest all = 1
// 获取 chunk二叉树根节点可分配深度能力值
// 这里获取到的便是 0(这是第一次的情况)
// 可分配深度能力值越小, 表示能够去分配的内存也就越多; 反之, 则越少
byte val = value(id);
// 条件成立 - 表示当前 chunk内剩余的内存不足以支撑此次内存的申请分配
if (val > d) { // unusable
// 返回 -1, 表示分配失败
return -1;
}
// 来到这, 说明 chunk中有足够的内存可满足此次内存的申请分配
// 深度优先的一个搜索算法
// 条件一:val < d, val表示的是二叉树中某个节点的可分配深度能力值, 条件成立说明当前节点管理的内存比较大, 总之大于申请容量的,
// 需要到当前节点的下一级尝试申请
// 条件二(此时 val >= d, 这里主要来判断正好能够完全分配的情况 - 找到对应深度的对应节点):对于 id对应的节点在深度为 d时, 此时 id & initial == 1;
// 对于 < d而言, 此时会是 0, 即条件成立
while (val < d || (id & initial) == 0) { // id & initial == 1 << d for all ids at depth d, for < d it is 0
// 到下一级去查找节点(靠左节点)
// id == 1, -> 2
id <<= 1;
// 获取 id对应节点的可分配深度能力值
val = value(id);
// 条件成立 - (当前节点已经分配内存出去了, 不够分配啦)接着便是来去获取兄弟节点了(对于每个节点, 只会有两个子节点)
if (val > d) {
// 伙伴算法, 来去寻找兄弟节点
// 如:2048 - 0b 0000 0000 0000 0000 0000 1000 0000 0000
// ^ 0b 0000 0000 0000 0000 0000 0000 0000 0001
// = 0b 0000 0000 0000 0000 0000 1000 0000 0001
id ^= 1;
// 获取兄弟节点的可分配深度能力值
val = value(id);
}
} // 出循环, 表示已经找到了可以去分配内存的节点 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);
// 占用 id对应的节点, 将其深度能力值设置为 unusable - 12, 表示已经分配内存了
setValue(id, unusable); // mark as unusable
// 向上更新父节点的可分配深度能力值(因为当前节点对应内存被分配出去了)
// 参数:分配内存的节点对应的 id
updateParentsAlloc(id);
// 这里返回的便是分配给业务使用的 Node对应的 id
return id;
}
这块先是去获取根节点 (id = 1)的深度能力值,判断当前二叉树是否还有空闲内存进行分配,如果有空闲内存进行分配的话,会通过深度优先算法到对应深度的层次是,当然首先会是最靠左的节点,因为此时的节点可能已经被分配出去了,因此这块还通过 "伙伴算法"去找其兄弟节点,若是有空闲内存的话,这最终会找到一个空闲的节点的
找到了节点,便需要去占用,表示该节点已经被分配出去了,对应 setValue(id,unusable)
private void setValue(int id, byte val) {
memoryMap[id] = val;
}
可以看到,就是去将 id对应的节点的深度能力值变为 unusable (12),这便表示着该节点已经分配出去了
还没完!因为二叉树表示的是 chunk中内存的使用情况,因此当这个节点分配出去了以后,需要去更新其父节点的可分配的深度能力值,同理,父父节点、祖父节点一样如此
对应的便是,updateParentsAlloc(id)
/**
* Update method used by allocate
* This is triggered only when a successor is allocated and all its predecessors
* need to update their state
* The minimal depth at which subtree rooted at id has some free space
*
* @param id id
*/
// 向上更新父节点的可分配深度能力值(因为当前节点对应内存被分配出去了)
// 参数:分配内存的节点对应的 id
private void updateParentsAlloc(int id) {
// 比如, id == 2048, 这会影响到:1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1
while (id > 1) {
// 获取父节点对应 id
// 如:2048 >>> 1 -> 1024
int parentId = id >>> 1;
// 获取 id is 1024的左右子节点的可分配深度能力值: 如:2048(12), 2049(11)
byte val1 = value(id);
byte val2 = value(id ^ 1);
// 获取左右子节点可分配深度值最小值
byte val = val1 < val2 ? val1 : val2;
// 更新父节点的可分配深度能力值为左右子节点中的最小可分配深度能力值
setValue(parentId, val);
// 更新 id, 继续往上更新
id = parentId;
}
}
继续回到 allocateRun(),后面便是去更新当前 chunk剩余空余内存了,因为分配出去了 id对应节点了嘛
即,freeBytes -= runLength(id);
// 返回节点 id所管理的内存宽度
// 假设 id == 2049
private int runLength(int id) {
// represents the size in #bytes supported by node 'id' in the tree
// log2ChunkSize - depth(id) -> 24 - 11 == 13
// 1 << 13 == 8kb
return 1 << log2ChunkSize - depth(id);
// 2048 -> 8k, 1024 -> 16k
}
然后返回值被赋予了 handle,表示的便是一个偏移量
接着继续看 chunk.allocate()的另一主线逻辑,对应 allocateSubpage()
/**
* Create / initialize a new PoolSubpage of normCapacity
* Any PoolSubpage created / initialized here is added to subpage pool in the PoolArena that owns this PoolChunk
*
* @param normCapacity normalized capacity
* @return index in memoryMap
*/
// 说明业务需要的内存是小规格的内存:tiny、small(业务请求分配内存量 <= 4kb)
// 由函数名也知道, 这是从 chunk上去申请一页内存
private long 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.
// 参数:规格化后的 size
// 寻找规格化后 size在 Arena中对应的两个 Subpages数组中其中一个的对应位置处的桶位处的 head节点
PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity);
// d == 11, 接下来要从 chunk上申请一页内存; 而二叉树叶子节点管理的便是一页, 因此 subpages只能从叶子节点出进行分配, so, 这里 d设置为了 11
int d = maxOrder; // subpages are only be allocated from pages i.e., leaves
// head作为锁
synchronized (head) {
// allocateNode(), 根据传入的深度值, 去二叉树上占用一个合适的节点, 并返回节点对应 id
// 这里传入的是叶子节点的深度值, 表示去申请一页内存, 因此这里会返回叶子节点深度值, 或者 -1
int id = allocateNode(d);
// true - 申请失败, chunk连一页内存都无法分配了
if (id < 0) {
return id;
}
// subpages数组是一个长度为 2048的数组, 保证创建出来的 subpages都有地方存放
final PoolSubpage<T>[] subpages = this.subpages;
// 8k
final int pageSize = this.pageSize;
// 占用了一个页, 因此这里来更新下 chunk中剩余的空闲内存
freeBytes -= pageSize;
// 取模运算:2048 -> 0, 2049 -> 1
int subpageIdx = subpageIdx(id);
// 获取指定位置的 subpage
PoolSubpage<T> subpage = subpages[subpageIdx];
// 正常情况下, subpage == null
if (subpage == null) {
// 这里来创建出了 PoolSubpage对象
// 参数一:规格化后 size在 Arena中对应的两个 Subpages数组中其中一个的对应位置处的桶位处的 head节点
// 这里将 head传了进去, 后续肯定会其一些作用的
// 参数二:当前 chunk对象, Subpage需要知道自己归属的 "爸爸"是谁
// 参数三:当前 subpage占用的叶子节点 id
// 参数四:这里计算出 id对应的一个偏移量, 2048 -> 0, 2049 -> 8k, 2050 -> 16k
// 其实就是当前叶子节点所管理的内存在整个内存中的 "偏移位置"
// 参数五:pageSize - 8k
// 参数六:规格化后的 size
subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity);
subpages[subpageIdx] = subpage;
} else {
subpage.init(head, normCapacity);
}
// 至此, 已经将 subpage创建出来了, 还没有到 subpage上去分配内存!(subpage可能是刚创建出来的, 也可能是已经存在了的, 通常情况下是前一种)
// 在 subpage上去申请分配小规格内存:small、tiny
return subpage.allocate();
}
}
因为对应的是 tiny、small小规格类型 size的申请分配,这块对应的便是到 chunk中去申请一页内存
即 Subpage的申请,而 Subpage对应的便是二叉树的叶子节点,8kb
即,对应的便是 Subpage的创建,这里会传进 head节点,而 head节点对应的便是规格化的 size在 Arena中对应 SubpagePools数组的对应桶位处的 head节点,由 arena.findSubpagePoolHead(normCapacity)
获取
// 参数:规格化后的 size
// 寻找规格化后 size在 Arena中对应的两个 Subpages数组中其中一个的对应位置处的桶位处的 head节点
PoolSubpage<T> findSubpagePoolHead(int elemSize) {
// 指向 pools数组下标符合当前 elemSize的桶
int tableIdx;
// 两个可选项:TinySubpagePools、SmallSubpagePools
PoolSubpage<T>[] table;
// 接下来便是去查找指定 table、tableIdx
// true - 规格化后的 size是 tiny类型
if (isTiny(elemSize)) { // < 512
// 除以 16, 得到指定数组下标
tableIdx = elemSize >>> 4;
// 赋值操作
table = tinySubpagePools;
} else {
// 1024的话, 这里对应的便是 1, 代入法计算即可
tableIdx = 0;
elemSize >>>= 10;
while (elemSize != 0) {
elemSize >>>= 1;
tableIdx ++;
}
table = smallSubpagePools;
}
// 返回符合当前规格 size的对应 table的对应桶位处的 head节点(head节点创建时是指向自身的 subpage对象)
return table[tableIdx];
}
接着便是去进行一页内存的申请,即到二叉树叶子节点那里获取一个尚未内占用的叶子节点,然后返回其 id,此 id将做为要创建的 Subpage对应的叶子节点
Subpage其实便是对 8k进行精细粒度的管理
然后,便是根据获取的元信息去创建 PoolSubpage
// 这里来创建出了 PoolSubpage对象
// 参数一:规格化后 size在 Arena中对应的两个 Subpages数组中其中一个的对应位置处的桶位处的 head节点
// 这里将 head传了进去, 后续肯定会其一些作用的
// 参数二:当前 chunk对象, Subpage需要知道自己归属的 "爸爸"是谁
// 参数三:当前 subpage占用的叶子节点 id
// 参数四:这里计算出 id对应的一个偏移量, 2048 -> 0, 2049 -> 8k, 2050 -> 16k
// 其实就是当前叶子节点所管理的内存在整个内存中的 "偏移位置"
// 参数五:pageSize - 8k
// 参数六:规格化后的 size
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;
// pageSize / 16 是因为 tiny最小规格是 16b, 如果以最小规格来进行划分的话, 那么需要多少 bit才能表示出整个内存的占用情况:512
// / 64, 是因为 long占 8个字节, 一个字节占 8 bit, 这里计算出需要多少 long才能表示出整个位图:8
// so, 这里就去创建出长度为 8的 long数组
bitmap = new long[pageSize >>> 10]; // pageSize / 16 / 64
// 参数一:规格化后 size在 Arena中对应的两个 Subpages数组中其中一个的对应位置处的桶位处的 head节点
// 参数二:规格化后的 size
init(head, elemSize);
}
可以看到,这块创建出了长度为 8的 long数组,这其实便是 bitMap 位图,Netty使用位图来去表示 Subpage中内存的使用情况
init(head, elemSize)
// 参数一:规格化后 size在 Arena中对应的两个 Subpages数组中其中一个的对应位置处的桶位处的 head节点
// 参数二:规格化后的 size
void init(PoolSubpage<T> head, int elemSize) {
// true 表示当前 subpage是存活状态, false表示当前 subpage是释放状态, 不可以状态
doNotDestroy = true;
// subpage需要知道自己所管理着的内存规格 size - 这也将决定 subpage是以哪一种规格进行划分的, 这将决定位图的有效长度
this.elemSize = elemSize;
// 条件一般都会成立
if (elemSize != 0) {
// pageSize / elemSize 计算出 subpage按照 elemSize划分, 一共可划分出多少小块
// 如, eleSize = 32b -> 256
// maxNumElems赋值之后, 就不会再改变, 表示当前 subpage最多可给业务分配多少小块内存
// numAvail, 每对外划分出一小块内存, 该值都减一
maxNumElems = numAvail = pageSize / elemSize;
// nextAvail 申请内存时, 会去使用这个字段, 表示下一个可用的位图下标值
nextAvail = 0;
// maxNumElems / 64 这里来计算出当前 maxNumElems需要 long数组的多少来进行表示
// 如:当 elemSize = 32b时, 表示最多可给上层业务划分 256块内存, 而这 256 bit的表示,
// 需要 256 / 64 == 4 个 long来进行表示, 对应的便是 bitmapLength == 4
// 当 elemSize = 48b, 计算出来的 bitmapLength == 2, 但存在余数, 因此无法表示完全, 需要做些额外的操作
bitmapLength = maxNumElems >>> 6;
// 条件成立, 说明 maxNumElems >>> 6存在余数, 此时需要 bitmapLength加一
if ((maxNumElems & 63) != 0) {
bitmapLength ++;
}
// 初始化 bitmap的值, 设置为 0
for (int i = 0; i < bitmapLength; i ++) {
bitmap[i] = 0;
}
}
// 看, 这里终于使用到了 head节点
// 参数:规格化后 size在 Arena中对应的两个 Subpages数组中其中一个的对应位置处的桶位处的 head节点
// 将创建出来的 Subpage添加到 Arena范围内的 head的下一个位置
addToPool(head);
}
因为 eleSize表示的是当前 Subpage要去进行划分的规格,这块会去计算出当前 subpage按照此规格最多可向外划分出去的内存块数,赋值到 maxNumElems 和 numAvail中去,前者固定值,后者随着每次内存的划分出去,该值都减一
接着便是一个 nextAvail,申请内存时, 会去使用这个字段, 表示下一个可用的位图下标值
接着,便是一个关键点,传进来的 head节点发挥作用了!
addToPool(head)
// 看, 这里终于使用到了 head节点
// 参数:规格化后 size在 Arena中对应的两个 Subpages数组中其中一个的对应位置处的桶位处的 head节点
// 将创建出来的 Subpage添加到 Arena范围内的 head的下一个位置
private void addToPool(PoolSubpage<T> head) {
assert prev == null && next == null;
prev = head;
next = head.next;
next.prev = this;
// 连接起来, 此时 head.next != head了
// 表示head所在桶位已经添加过对应规格 size的 Subpage了
head.next = this;
}
将创建出来的 Subpage添加到 Arena范围内的 head的下一个位置
接着,继续回到主逻辑:allocateSubpage()
Subpage创建出来后,接下来便是到 subpage上去进行 tiny、small类型 size的分配操作了
/**
* Returns the bitmap index of the subpage allocation.
*/
// 在 subpage上去申请分配小规格内存:small、tiny
long allocate() {
if (elemSize == 0) {
return toHandle(0);
}
// 条件一:numAvail == 0, 说明当前 subpage管理的一页内存全部划分出去了, 没有空余内存来划分了
// 条件二:!doNotDestroy, doNotDestroy初始值为 true, 当 subpage释放内存后, doNotDestroy会被改为 false
// 条件成立 - 当前 subpage已经无法进行内存分配了, 这里相当于 "短路操作"
if (numAvail == 0 || !doNotDestroy) {
return -1;
}
// 从 bitmap上去找一个可用的 bit, 返回该 bit的索引
final int bitmapIdx = getNextAvail();
// 下面逻辑便是去将 bitmapIdx表示的 bit设置为 1, 表示这块内存已经划分出去了
// 如:bitmapIdx = 68, 这里计算出来的 q = 1
// 这里其实就是计算出 bitmapIdx在 bitmap数组中的哪个 long中
// 即, bitmap[q]
int q = bitmapIdx >>> 6;
// 这里计算的便是 bitmapIdx在 long所表示的 64个 bit中的哪个 bit
// 以 68位例, r = 4
int r = bitmapIdx & 63;
assert (bitmap[q] >>> r & 1) == 0;
// 代入法, 1L << 4 = 16
// 0b 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 1111
// 0b 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 0000
// 0b 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001 1111
// 这便完成了对 bitmapIdx对应位置的 bit的修改, 设置成了 1, 表示这块内存已经分配出去了
bitmap[q] |= 1L << r;
// -- numAvail 更新可对外分配的内存块数(因为刚刚划分出去了一小块内存)
// 条件成立, 说明当前 subpage中已经没有可分配的内存了
if (-- numAvail == 0) {
// 从当前 subpage归属的 chunk归属的 arena中移除当前 subpage, 因为当前 subpage管理的内存已经全部都划分出去了,
// 其它业务也无法再从该 subpage上申请内存成功了...
removeFromPool();
}
// bitmapIdx - 刚业务划分出去内存对应的位图索引值
return toHandle(bitmapIdx);
}
详细可以去看注释,这里的主逻辑其实到 bitmap上找到一个可用点 bit,去占用它,设置其 bit为 1,表示该 bit表示的内存已经分配出去了
先是 bit对应 索引的查找,对应 getNextAvail()
// 从 bitmap上寸照一个可用的 bit, 返回该 bit的索引
private int getNextAvail() {
// nextAvail 初始值给的是 0, 当某个 ByteBuf占用的内存还给当前 Subpage时, 这个内存占用的 bit的索引值会被设置到 this.nextAvail里头
// 下次再去申请时, 直接去使用 nextAvail即可
int nextAvail = this.nextAvail;
// 条件成立:对应两种情况
// a.初始值 nextAvail == 0的情况
// b.当其它 byteBuf归还内存时去设置 nextAvail为它所占用的那块内存对应的 bit索引值
if (nextAvail >= 0) {
this.nextAvail = -1;
return nextAvail;
}
// 一般情况下, 我们都会走到这里
// 这块其实就是到 bitmap中去遍历查找可使用的 bit索引值的情况
return findNextAvail();
}
private int findNextAvail() {
// 获取当前 subpage对应的位图
final long[] bitmap = this.bitmap;
// 获取位图数组的有效长度
final int bitmapLength = this.bitmapLength;
// 假设当前 subpage(规格是 32b)对外提供了 68块小内存, 并且着 68块小内存都没归还给 subpage, 那它的位图长什么样 ?
// bitmap[0] = 0b 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111
// bitmap[1] = 0b 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 1111
// bitmap[2] = 0b 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
// bitmap[3] = 0b 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
for (int i = 0; i < bitmapLength; i ++) {
long bits = bitmap[i];
// 条件不成立:说明 bitmap[i]表示的这块内存已经全部都分配出去了... 没办法在 bitmap[i]的这个 bitmap上分配了
if (~bits != 0) {
// 来到这, 说明 bitmap[i]上还有空余空间支持此次 allocate分配内存操作
// 参数一:i
// 参数二:bitmap[i]表示的这一小块 bitmap
// findNextAvail0()去寻找一个空闲内存的 bitmap的索引值
return findNextAvail0(i, bits);
}
}
// 返回 -1, 说明整个 subpage都被占用完毕了... 无法完成分配
return -1;
}
// 参数一:i
// 参数二:bitmap[i]表示的这一小块 bitmap
private int findNextAvail0(int i, long bits) {
// 表示当前 subpage最多可对外分配的内存块数, 假设 subpage规格为 32b, 则 maxNumElems == 256
final int maxNumElems = this.maxNumElems;
// 假设当前 subpage(规格是 32b)对外提供了 68块小内存, 并且着 68块小内存都没归还给 subpage, 那它的位图长什么样 ?
// bitmap[0] = 0b 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111
// bitmap[1] = 0b 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 1111
// bitmap[2] = 0b 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
// bitmap[3] = 0b 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
// 以这个例子来说的话, 这里的 baseVal -> 1 << 6 -> 64
final int baseVal = i << 6;// 不得不说, 细想这一块设计得是真的好
// 循环, 从 bitmap[1]中找到第一个可以用的 bit位置, 返回给业务该 bit位置对应的索引值
for (int j = 0; j < 64; j ++) {
// 0b 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 1111
// 0b 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0001
// 显然第一次条件不成立
if ((bits & 1) == 0) { // 以样例来看, 此时 j == 4
// 64 | 4 -> 68
// 这块相当于是去做了个加法运算(因为此时 baseUrl低位全部都为 0, so, 这块使用 |来去做加法运算)
int val = baseVal | j;
if (val < maxNumElems) {
// 返回对应 bit在位图中的坐标值
// 以样例来说, 这里会是 68
return val;
} else {
break;
}
}
// 无符号右移:0b 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 1111
// 这里对应的其实就是逐位去排除已使用的 bit, 最终找到可用的 bit, 此时, 上述条件将成立
// 对应的便是:0b 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
bits >>>= 1;
}
return -1;
}
至此,便获取到了 bitMap上空闲的 bit对应的索引值,然后设置其 bit为 1即可
这里其实还有一个细节,便是当 subpage可分配的内存块数都已经分配出去后,当前 subpage会从 Arena中移除出去,因为此时其它业务无法再从该 subpage上申请内存成功了嘛,对应 removeFromPool()
// 从当前 subpage归属的 chunk归属的 arena中移除当前 subpage, 因为当前 subpage管理的内存已经全部都划分出去了,
// 其它业务也无法再从该 subpage上申请内存成功了...
private void removeFromPool() {
assert prev != null && next != null;
prev.next = next;
next.prev = prev;
next = null;
prev = null;
}
为了使返回值和 allocateRun()的有区别,这里还去做了些许处理:tohandle(bitmapIdx)
详细看注释:
// bitmapIdx - 刚业务划分出去内存对应的位图索引值
private long toHandle(int bitmapIdx) {
// 假设 bitmapIdx = 68
// (long) bitmapIdx << 32
// 0b 0000 0000 0000 0000 0000 0000 0100 0100 0000 0000 0000 0000 0000 0000 0000 0000
// 可以看到, 现在这个值:高 32位 bitmapIdx的值, 低 32位全是 0
// memoryMapIdx 是当前 subpage对应叶子节点的 id, 这里假设是 2049
// 经过 | memoryMapIdx后
// 高 32位是 bitMapIdx的值, 低 32位是当前 subpage对应的叶子节点的 id值
// 即, 0b 0000 0000 0000 0000 0000 0000 0100 0100 0000 0000 0000 0000 0000 1000 0000 0001
// 0x4000000000000000L -> 0100 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000(除符号位后, 最高位为 1, 其它都是 0)
// 二者经过 |运算后, 最终得到:0b 0100 0000 0000 0000 0000 0000 0100 0100 0000 0000 0000 0000 0000 0000 0000 0000‘
// 这里为什么要来操作呢 ?
// 从 chunk中分配内存对应两个主逻辑:>= 8k, < 8k的, 分别去调用了 allocateRun()、allocateSubpage(), 都分别会返回一个 handle
// 这块这么做便是为了区别不同逻辑从 chunk分配内存时返回值代表的操作的不同, 因此, 这块去设置了除符号位后最高位为 1(其实就是个 "标志位")
// 以此来对二者进行区分, 区分之后
// 后面会根据 handle的值去创建 byteBuf对象, 需要根据 handle的值计算出 byteBuf共享 chunk ByteBuffer内存的偏移位置
// allocateRun()、allocateSubpage()申请的内存, 计算规则完全不一样, 需要根据 handle的值进行不同的逻辑处理
// 如:当 subpage第一次对外分配内存时, 返回的 handle如果没有标志位的话, 会与 allocateRun()返回的相冲突
// subpage占用的叶子节点是 2048, 第一次对外分配内存时返回的值为:高 32位是 0, 低 32位是 2048
// 这不就可能会与 allocateRun()相冲突了吗 ?
// so, 标识位的引入必不可少
return 0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx;
}
此时会到主逻辑,chunk.allocate()
// 参数一:buf, 返回给业务属于的 ByteBuf对象, 下面逻辑会给该 buf分配真正的内存
// 参数二:指定想要去分配的内存容量
// 参数三:规格化后的 size
// 注:16mb以及以内的内存能够到 chunk上进行分配
// 从 chunk中申请内存正对应着两种逻辑:>= 8k, < 8k
boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
// 这是一个非常重要的值
final long handle;
// true - 规格化后的 size >= 8kb - 业务需求量大于等于 pageSize(即 8kb, 16kb, 32kb...)
if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize
// Normal类型规格内存的申请(小于等于 16mb)
// 这里的 handler是当前分配节点占用的 树节点对应的 id, 可能是 -1, -1表示申请失败
handle = allocateRun(normCapacity);
} else { // 业务需求量小于 pageSize(tiny、small规格类型)
// 说明业务需要的内存是小规格的内存:tiny、small
// 这里便是去申请一夜内存的逻辑
// 这里的 handler是当前分配节点占用的 树节点对应的 id, 可能是 -1, -1表示申请失败
handle = allocateSubpage(normCapacity);
}
// 执行到这里, 会拿到一个 handle值, 该值可能是 allocateRun() 或 allocateSubpage()返回值
// 条件成立, 说明内存申请分配失败 -1
if (handle < 0) {
return false;
}
// 暂不考虑了, 这里假设获取的是 null
ByteBuffer nioBuffer = cachedNioBuffers != null ? cachedNioBuffers.pollLast() : null;
// 参数一:byteBuf, 返回给上层业务的 ByteBuf对象, 需要给该 buf分配真正的内存
// 参数二:bull
// 参数三:allocateRun()或 allocateSubpage()返回的 handle
// 参数四:指定想要去分配的内存容量
// 核心方法
initBuf(buf, nioBuffer, handle, reqCapacity);
// 来到这, 已经为 buf设置好内存信息了, 上层业务可以使用了
return true;
}
由前述,我们知道已经是获取到了一个 bytebuf,但还没有去为其设置内存信息,so,这里根据获取的 handle去设置其内存相关信息了
// 参数一:byteBuf, 返回给上层业务的 ByteBuf对象, 需要给该 buf分配真正的内存
// 参数二:bull
// 参数三:allocateRun()或 allocateSubpage()返回的 handle
// 参数四:指定想要去分配的内存容量
// 核心方法
void initBuf(PooledByteBuf<T> buf, ByteBuffer nioBuffer, long handle, int reqCapacity) {
// 表示 handle低 32位的值 - 其实就是内存分配占用的节点对应的 id
int memoryMapIdx = memoryMapIdx(handle);
// 表示 handle高 32位的值 - 对应的便是 内存分配对应的位图索引值(该值区分 allocateRun()、allocateSubpage())
int bitmapIdx = bitmapIdx(handle);
// CASE1: handle是 allocateRun()的返回值
if (bitmapIdx == 0) { // 来处理 allocateRun()内存封装的逻辑
// 获取此次占用的节点对应的可分配深度能力值
byte val = value(memoryMapIdx);
// 这里就是确保节点可用
assert val == unusable : String.valueOf(val);
// 参数一:this, 创建 ByteBuf分配内存的 chunk对象, 真实的内存是由 chunk进行管理的, 因此必须要去传 chunk对象
// 参数二:null
// 参数三:handle, allocateRun()方法返回值, buf占用的内存位置相关信息都与 handle值有关系, 后面释放内存时还要使用 handle的值
// 参数四:runOffset(memoryMapIdx) + offset -> 计算出当前 buf占用的内存在 byteBuffer上的偏移位置,
// 必须知道 buf管理内存在 大内存上的偏移位置
// 参数五:指定想要去分配的内存容量(后续需要赋给 buf的 length属性)
// 参数六:返回节点 id所管理的内存宽度(2048 -> 8k, 1024 -> 16k),
// 该值最终会被赋予到 byteBuf的 maxLength字段, 表示 buf可用内存的最大大小, 如申请 11k, 这里会是 16k
// 参数七:与当前线程相关的 PoolThreadCache对象 - 本地缓存
// 为什么要传递这个参数呢 ? - 因为后面释放 byteBuf时, 首选释放的地方为 threadLocalCache, 即缓存到线程局部中去,
// 方便后续申请时使用 (get即可), 而不是直接归还到 chunk中去(即对应原本其应该在的地方 - pool)
buf.init(this, nioBuffer, handle, runOffset(memoryMapIdx) + offset,
reqCapacity, runLength(memoryMapIdx), arena.parent.threadCache());
}
// CASE2: handle是 allocateSubpage()返回值
else { // 执行 allocateSubpage()内存封装的逻辑
// 参数一:byteBuf, 返回给上层业务的 ByteBuf对象, 需要给该 buf分配真正的内存
// 参数二:null
// 参数三:handle, allocateSubpage()方法返回值, buf占用的内存位置相关信息都与 handle值有关系, 后面释放内存时还要使用 handle的值
// 参数四: 内存分配对应的位图索引值 - 从高位起的第二位是标志位, 这里是 1
// 参数五:指定想要去分配的内存容量
initBufWithSubpage(buf, nioBuffer, handle, bitmapIdx, reqCapacity);
}
// 来到这, 说明 buf中内存信息已经设置完毕了, 上层业务可以直接去使用了
}
这里根据 handle是哪个方法的返回值去做了相应的处理,tiny与 small对应的 handle与 normal对应的 handle是要去做不同的内存封装逻辑处理的
咋们先去看 allocateRun()返回值的逻辑
对应:
// 参数一:this, 创建 ByteBuf分配内存的 chunk对象, 真实的内存是由 chunk进行管理的, 因此必须要去传 chunk对象
// 参数二:null
// 参数三:handle, allocateRun()方法返回值, buf占用的内存位置相关信息都与 handle值有关系, 后面释放内存时还要使用 handle的值
// 参数四:runOffset(memoryMapIdx) + offset -> 计算出当前 buf占用的内存在 byteBuffer上的偏移位置,
// 必须知道 buf管理内存在 大内存上的偏移位置
// 参数五:指定想要去分配的内存容量(后续需要赋给 buf的 length属性)
// 参数六:返回节点 id所管理的内存宽度(2048 -> 8k, 1024 -> 16k),
// 该值最终会被赋予到 byteBuf的 maxLength字段, 表示 buf可用内存的最大大小, 如申请 11k, 这里会是 16k
// 参数七:与当前线程相关的 PoolThreadCache对象 - 本地缓存
// 为什么要传递这个参数呢 ? - 因为后面释放 byteBuf时, 首选释放的地方为 threadLocalCache, 即缓存到线程局部中去,
// 方便后续申请时使用 (get即可), 而不是直接归还到 chunk中去(即对应原本其应该在的地方 - pool)
buf.init(this, nioBuffer, handle, runOffset(memoryMapIdx) + offset,
reqCapacity, runLength(memoryMapIdx), arena.parent.threadCache());
接着,来到:pooledByteBuf.init()
// 根据前面可知, 这里的范式 T是 ByteBuffer
void init(PoolChunk<T> chunk, ByteBuffer nioBuffer,
long handle, int offset, int length, int maxLength, PoolThreadCache cache) {
// 参数一:this, 创建 ByteBuf分配内存的 chunk对象, 真实的内存是由 chunk进行管理的, 因此必须要去传 chunk对象
// 参数二:null
// 参数三:handle, allocateRun()方法返回值, buf占用的内存位置相关信息都与 handle值有关系, 后面释放内存时还要使用 handle的值
// 参数四:runOffset(memoryMapIdx) + offset -> 计算出当前 buf占用的内存在 byteBuffer上的偏移位置,
// 必须知道 buf管理内存在 大内存上的偏移位置
// 参数五:指定想要去分配的内存容量(后续需要赋给 buf的 length属性)
// 参数六:返回节点 id所管理的内存宽度(2048 -> 8k, 1024 -> 16k),
// 该值最终会被赋予到 byteBuf的 maxLength字段, 表示 buf可用内存的最大大小, 如申请 11k, 这里会是 16k
// 参数七:与当前线程相关的 PoolThreadCache对象 - 本地缓存
// 为什么要传递这个参数呢 ? - 因为后面释放 byteBuf时, 首选释放的地方为 threadLocalCache, 即缓存到线程局部中去,
// 方便后续申请时使用 (get即可), 而不是直接归还到 chunk中去(即对应原本其应该在的地方 - pool)
init0(chunk, nioBuffer, handle, offset, length, maxLength, cache);
}
// 参数一:this, 创建 ByteBuf分配内存的 chunk对象, 真实的内存是由 chunk进行管理的, 因此必须要去传 chunk对象
// 参数二:null
// 参数三:handle, allocateRun()方法返回值, buf占用的内存位置相关信息都与 handle值有关系, 后面释放内存时还要使用 handle的值
// 参数四:runOffset(memoryMapIdx) + offset -> 计算出当前 buf占用的内存在 byteBuffer上的偏移位置,
// 必须知道 buf管理内存在 大内存上的偏移位置
// 参数五:指定想要去分配的内存容量(后续需要赋给 buf的 length属性)
// 参数六:返回节点 id所管理的内存宽度(2048 -> 8k, 1024 -> 16k),
// 该值最终会被赋予到 byteBuf的 maxLength字段, 表示 buf可用内存的最大大小, 如申请 11k, 这里会是 16k
// 参数七:与当前线程相关的 PoolThreadCache对象 - 本地缓存
// 为什么要传递这个参数呢 ? - 因为后面释放 byteBuf时, 首选释放的地方为 threadLocalCache, 即缓存到线程局部中去,
// 方便后续申请时使用 (get即可), 而不是直接归还到 chunk中去(即对应原本其应该在的地方 - pool)
private void init0(PoolChunk<T> chunk, ByteBuffer nioBuffer,
long handle, int offset, int length, int maxLength, PoolThreadCache cache) {
assert handle >= 0;
assert chunk != null;
this.chunk = chunk;
// memory其实就是 chunk中共享的 16mb那块内存 - DirectByteBuffer
memory = chunk.memory;
tmpNioBuf = nioBuffer;
allocator = chunk.arena.parent;
this.cache = cache;
this.handle = handle;
// 下面这三个字段是关键 - 通过 offset、length、maxLength, 我们便知道 ByteBuf偏移位置、长度、最大可用大小分别是什么了!
this.offset = offset;
this.length = length;
this.maxLength = maxLength;
}
可以看到,当 init0()执行完后,buf内存信息就设置完成了,上层业务就可以去进行使用了
buf的内存信息如何进行表示?根据偏移量 offset、长度 length、最大可用长度 maxLength,这不就能表示其在池化内存 DirectByteBuffer上的管理的内存了么!
再回去主逻辑,对应 handle是 allocateSubpage()返回值的内存封装逻辑
对应 initBufWithSubpage()方法调用
// 参数一:byteBuf, 返回给上层业务的 ByteBuf对象, 需要给该 buf分配真正的内存
// 参数二:null
// 参数三:handle, allocateSubpage()方法返回值, buf占用的内存位置相关信息都与 handle值有关系, 后面释放内存时还要使用 handle的值
// 参数四: 内存分配对应的位图索引值 - 从高位起的第二位是标志位, 这里是 1
// 参数五:指定想要去分配的内存容量
initBufWithSubpage(buf, nioBuffer, handle, bitmapIdx, reqCapacity);
// 参数一:byteBuf, 返回给上层业务的 ByteBuf对象, 需要给该 buf分配真正的内存
// 参数二:null
// 参数三:handle, allocateSubpages()方法返回值, buf占用的内存位置相关信息都与 handle值有关系, 后面释放内存时还要使用 handle的值
// 参数四: 内存分配对应的位图索引值 - 从高位起的第二位是标志位, 这里是 1
// 参数五:指定想要去分配的内存容量
private void initBufWithSubpage(PooledByteBuf<T> buf, ByteBuffer nioBuffer,
long handle, int bitmapIdx, int reqCapacity) {
assert bitmapIdx != 0;
// 获取 subpage占用的叶子节点 id
int memoryMapIdx = memoryMapIdx(handle);
// 获取给该 buf分配内存的 subpage对象
PoolSubpage<T> subpage = subpages[subpageIdx(memoryMapIdx)];
assert subpage.doNotDestroy;
assert reqCapacity <= subpage.elemSize;
// 参数一:this, 创建 ByteBuf分配内存的 chunk对象, 真实的内存是由 chunk进行管理的, 因此必须要去传 chunk对象
// 参数二:null
// 参数三:handle, allocateSubpage()方法返回值, buf占用的内存位置相关信息都与 handle值有关系, 后面释放内存时还要使用 handle的值
// 参数四:业务申请的这一小块内存在 chunk bytebuffer大内存上的一个偏移量
// 参数五:指定想要去分配的内存容量(后续需要赋给 buf的 length属性)
// 参数六:当前 subpage进行划分的规格 size
// 参数七:与当前线程相关的 PoolThreadCache对象 - 本地缓存
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
// runOffset(memoryMapIdx) 计算出 subpage占用的内存在 ByteBuffer中的偏移量
// (bitmapIdx & 0x3FFFFFFF) -> 0x3FFFFFFF -> 0b 0011 1111 1111 1111 1111 1111 1111 1111
// 假设 bitmapIdx is 68 -> 0b 0100 0000 0000 0000 0000 0000 0100 0100 & 0b 0011 1111 1111 1111 1111 1111 1111 1111
// 得到 0b 0000 0000 0000 0000 0000 0000 0100 0100, 这块就是去做个 "解码操作"嘛, 清除标志位
// 之后, * subpage.elemSize, 这便得到了分配给业务的这一小块内存 bitMapIdx 在当前 subpage 上的偏移量
// 因此, 这块就计算出来了业务申请的这一小块内存在 chunk bytebuffer大内存上的一个偏移量
}
可以看到,不管是 normal还是 small、tiny,都需要获取分配的内存在大内存上的一个偏移量,后续才能对 ByteBuf进行内存信息的设置
接下来便是相同逻辑的处理了
接着回到 allocateNormal()逻辑,可以看到:
qInit.add(c)
将新创建出来的 chunk添加到 qInit chunkList中去
这就完成了 chunk中内存的分配以及 chunk中内存的处理
至此,内存分配解读完毕,我们的脑海里也有了池化内存结构图了吧,hh
涨知识了