上次讲Jemalloc 算法将每个 Chunk 切分成多个小块 Page,但是Page还是较大的内存块,所以直接使用仍旧浪费。因此,Jemalloc 算法将每个 Page 更进一步的切分为多个 Subpage 内存块。Page 切分成多个 Subpage 内存块,直接基于数组,通过数组来标记每个 Subpage 内存块是否已经分配,这里用PoolSubpage解决这个问题。
一个page切分成多个大小均等的Subpage内存块。每个 Page 拆分的 Subpage 内存块可以不同,以 Page 第一次拆分为 Subpage 内存块时请求分配的内存大小为准。(比如说,申请一个 16B 的内存块,那么 Page0 被拆成成 ( 8KB / 16B )512个 Subpage 块,使用第 0 块。然后,申请一个 32B 的内存块,那么 Page1 被拆分成 ( 8KB / 32B )256个 Subpage 块,使用第 0 块。最后,申请一个 16B 的内存块,那么重用 Page0 ,使用第 1 块。看不明白的话去上一篇看图就明白了嗷)
总结一下就是说Subpage内存块被申请时,先去找大小匹配且可分配的page,如果有就使用其中一块Subpage,如果没有则选择下一个新的page进行拆分,使用新Page的第0块Subpage。
PoolSubpage ,实现 PoolSubpageMetric 接口,Netty 对 Jemalloc Subpage 的实现类。
构造方法:
/**
* 所属 PoolChunk 对象
*/
final PoolChunk<T> chunk;
/**
* 在 {@link PoolChunk#memoryMap} 的节点编号
*/
private final int memoryMapIdx;
/**
* 在 Chunk 中,偏移字节量
*
* @see PoolChunk#runOffset(int)
*/
private final int runOffset;
/**
* Page 大小 {@link PoolChunk#pageSize}
*/
private final int pageSize;
/**
* Subpage 分配信息数组
*
* 每个 long 的 bits 位代表一个 Subpage 是否分配。
* 因为 PoolSubpage 可能会超过 64 个( long 的 bits 位数 ),所以使用数组。
* 例如:Page 默认大小为 8KB ,Subpage 默认最小为 16 B ,所以一个 Page 最多可包含 8 * 1024 / 16 = 512 个 Subpage 。
* 因此,bitmap 数组大小为 512 / 64 = 8 。
* 另外,bitmap 的数组大小,使用 {@link #bitmapLength} 来标记。或者说,bitmap 数组,默认按照 Subpage 的大小为 16B 来初始化。
* 为什么是这样的设定呢?因为 PoolSubpage 可重用,通过 {@link #init(PoolSubpage, int)} 进行重新初始化。
*/
private final long[] bitmap;
/**
* 双向链表,前一个 PoolSubpage 对象
*/
PoolSubpage<T> prev;
/**
* 双向链表,后一个 PoolSubpage 对象
*/
PoolSubpage<T> next;
/**
* 是否未销毁
*/
boolean doNotDestroy;
/**
* 每个 Subpage 的占用内存大小,例如 16B、32B 等等。
*/
int elemSize;
/**
* 总共 Subpage 的数量
*/
private int maxNumElems;
/**
* {@link #bitmap} 长度,bitmap 数组的真正使用的数组大小。
*/
private int bitmapLength;
/**
* 下一个可分配 Subpage 的数组位置
*/
private int nextAvail;
/**
* 剩余可用 Subpage 的数量
*/
private int numAvail;
// 构造方法 1: 双向链表,头节点
/** Special constructor that creates a linked list head */
PoolSubpage(int pageSize) {
chunk = null;
//memoryMapIdx 属性,在 PoolChunk.memoryMap 的节点编号,例如节点编号 2048 。
memoryMapIdx = -1;
//runOffset 属性,在 Chunk 中,偏移字节量,通过 PoolChunk的runOffset(id) 方法计算。在 PoolSubpage 中,无相关的逻辑,仅用于 toString() 方法,打印信息。
runOffset = -1;
elemSize = -1;
this.pageSize = pageSize;
bitmap = null;
}
// 构造方法 2: 双向链表,Page 节点
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 数组
bitmap = new long[pageSize >>> 10]; // pageSize / 16 / 64
// 初始化
init(head, elemSize);
}
init(PoolSubpage head, int elemSize) 方法,初始化:
void init(PoolSubpage<T> head, int elemSize) {
// 未销毁
doNotDestroy = true;
// 初始化 elemSize
this.elemSize = elemSize;
if (elemSize != 0) {
// 初始化 maxNumElems
maxNumElems = numAvail = pageSize / elemSize;
// 初始化 nextAvail
nextAvail = 0;
// 计算 bitmapLength 的大小
bitmapLength = maxNumElems >>> 6;
if ((maxNumElems & 63) != 0) { // 未整除,补 1.
bitmapLength ++;
}
// 初始化 bitmap
for (int i = 0; i < bitmapLength; i ++) {
bitmap[i] = 0;
}
}
// 添加到 Arena 的双向链表中。
addToPool(head);
}
在每个 Arena 中,有 tinySubpagePools 和 smallSubpagePools 属性,分别表示 tiny 和 small 类型的 PoolSubpage 数组(Subpage的内存规格分为Tiny 和 Small 两类,并且每类有多种大小,16B - 496B算Tiny,512B - 4KB算Small):
// PoolArena.java
/**
* tiny 类型的 PoolSubpage 数组
*
* 数组的每个元素,都是双向链表
*/
private final PoolSubpage<T>[] tinySubpagePools;
/**
* small 类型的 SubpagePools 数组
*
* 数组的每个元素,都是双向链表
*/
private final PoolSubpage<T>[] smallSubpagePools;
数组的每个元素,通过 prev 和 next 属性,形成双向链表。
每个元素,表示对应的 Subpage 内存规格的双向链表。
例如:tinySubpagePools[0] 表示 16B ,tinySubpagePools[1] 表示 32B 。
根据tinySubpagePools 和 smallSubpagePools 属性,可以从中查找,是否已经有符合分配内存规格的 Subpage 节点可分配。
初始时,每个双向链表,会创建对应的 head 节点:
// PoolArena.java
private PoolSubpage<T> newSubpagePoolHead(int pageSize) {
PoolSubpage<T> head = new PoolSubpage<T>(pageSize);
head.prev = head;
head.next = head;
return head;
}
head 的上下节点都是自己。(这是个循环链表。)
addToPool(PoolSubpage head) 方法中,添加到 Arena 的双向链表中:
private void addToPool(PoolSubpage<T> head) {
assert prev == null && next == null;
// 将当前节点,插入到 head 和 head.next 中间
prev = head;
next = head.next;
next.prev = this;
head.next = this;
}
removeFromPool() 方法中,从双向链表中移除:
private void removeFromPool() {
assert prev != null && next != null;
// 前后节点,互相指向
prev.next = next;
next.prev = prev;
// 当前节点,置空
next = null;
prev = null;
}
allocate() 方法,分配一个 Subpage 内存块,并返回该内存块的位置 handle :
long allocate() {
// 防御性编程,不存在这种情况。
if (elemSize == 0) {
return toHandle(0);
}
// 可用数量为 0 ,或者已销毁,返回 -1 ,即不可分配。
if (numAvail == 0 || !doNotDestroy) {
return -1;
}
// 获得下一个可用的 Subpage 在 bitmap 中的总体位置
final int bitmapIdx = getNextAvail();
// 获得下一个可用的 Subpage 在 bitmap 中数组的位置(bitmapIdx / 64)
int q = bitmapIdx >>> 6;
// 获得下一个可用的 Subpage 在 bitmap 中数组的位置的第几 bits(bitmapIdx % 64 )
int r = bitmapIdx & 63;
assert (bitmap[q] >>> r & 1) == 0;
// 修改 Subpage 在 bitmap 中不可分配。
bitmap[q] |= 1L << r;
// 可用 Subpage 内存块的计数减一
if (-- numAvail == 0) { // 无可用 Subpage 内存块
// 从双向链表中移除
removeFromPool();
}
// 计算 handle
return toHandle(bitmapIdx);
}
调用 toHandle(bitmapIdx) 方法,计算 handle 值。
private long toHandle(int bitmapIdx) {
return 0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx;
}
free(PoolSubpage head, int bitmapIdx) 方法,释放指定位置的 Subpage 内存块,并返回当前 Page 是否正在使用中( true ):
如果不再使用,可以将该节点( Page )从 Chunk 中释放,标记为可用
boolean free(PoolSubpage<T> head, int bitmapIdx) {
if (elemSize == 0) {
return true;
}
// 获得 Subpage 在 bitmap 中数组的位置
int q = bitmapIdx >>> 6;
// 获得 Subpage 在 bitmap 中数组的位置的第几 bits
int r = bitmapIdx & 63;
assert (bitmap[q] >>> r & 1) != 0;
// 修改 Subpage 在 bitmap 中可分配。
bitmap[q] ^= 1L << r;
// 设置下一个可用为当前 Subpage
setNextAvail(bitmapIdx);
// 可用 Subpage 内存块的计数加一
if (numAvail ++ == 0) {
// 添加到 Arena 的双向链表中。
addToPool(head);
return true;
}
// 还有 Subpage 在使用
if (numAvail != maxNumElems) {
return true;
// 没有 Subpage 在使用
} else {
// 双向链表中,只有该节点,不进行移除
// Subpage not in use (numAvail == maxNumElems)
if (prev == next) {
// Do not remove if this subpage is the only one left in the pool.
return true;
}
// 标记为已销毁
// Remove this subpage from the pool if there are other subpages left in the pool.
doNotDestroy = false;
// 从双向链表中移除
removeFromPool();
return false;
}
}
调用 setNextAvail(int bitmapIdx) 方法,设置下一个可用为当前 Subpage 的位置:
private void setNextAvail(int bitmapIdx) {
nextAvail = bitmapIdx;
}
getNextAvail() 方法,获得下一个可用的 Subpage 在 bitmap 中的总体位置:
private int getNextAvail() {
int nextAvail = this.nextAvail;
// nextAvail 大于 0 ,意味着已经“缓存”好下一个可用的位置,直接返回即可。
if (nextAvail >= 0) {
this.nextAvail = -1;
return nextAvail;
}
// 寻找下一个 nextAvail
return findNextAvail();
}
findNextAvail() 方法,寻找下一个 nextAvail:
private int findNextAvail() {
final long[] bitmap = this.bitmap;
final int bitmapLength = this.bitmapLength;
// 循环 bitmap
for (int i = 0; i < bitmapLength; i ++) {
long bits = bitmap[i];
// ~ 操作,如果不等于 0 ,说明有可用的 Subpage
if (~bits != 0) {
// 在这 bits 寻找可用 nextAvail
return findNextAvail0(i, bits);
}
}
// 未找到
return -1;
}
findNextAvail0(int i, long bits) 方法,在这 bits 寻找可用 nextAvail :
private int findNextAvail0(int i, long bits) {
final int maxNumElems = this.maxNumElems;
// 计算基础值,表示在 bitmap 的数组下标
final int baseVal = i << 6; // 相当于 * 64
// 遍历 64 bits
for (int j = 0; j < 64; j ++) {
// 计算当前 bit 是否未分配
if ((bits & 1) == 0) {
// 可能 bitmap 最后一个元素,并没有 64 位,通过 baseVal | j < maxNumElems 来保证不超过上限。
int val = baseVal | j;
if (val < maxNumElems) {
return val;
} else {
break;
}
}
// 去掉当前 bit
bits >>>= 1;
}
// 未找到
return -1;
}
destroy() 方法,销毁:
void destroy() {
if (chunk != null) {
chunk.destroy();
}
}
PoolSubpageMetric ,PoolSubpage Metric 接口:
public interface PoolSubpageMetric {
/**
* Return the number of maximal elements that can be allocated out of the sub-page.
*/
int maxNumElements();
/**
* Return the number of available elements to be allocated.
*/
int numAvailable();
/**
* Return the size (in bytes) of the elements that will be allocated.
*/
int elementSize();
/**
* Return the size (in bytes) of this page.
*/
int pageSize();
}
PoolChunk 对 PoolChunkMetric 接口的实现:
@Override
public int maxNumElements() {
synchronized (chunk.arena) {
return maxNumElems;
}
}
@Override
public int numAvailable() {
synchronized (chunk.arena) {
return numAvail;
}
}
@Override
public int elementSize() {
synchronized (chunk.arena) {
return elemSize;
}
}
@Override
public int pageSize() {
return pageSize;
}