前面文章说了PoolChunk如何管理Normal内存块,本文分享PoolSubpage如何管理Small内存块。

源码分析基于Netty 4.1.52

内存管理算法

PoolSubpage负责管理Small内存块。一个PoolSubpage中的内存块size都相同,该size对应SizeClasses#sizeClasses表格的一个索引index。

新创建的PoolSubpage都必须加入到PoolArena#smallSubpagePools[index]链表中。

PoolArena#smallSubpagePools是一个PoolSubpage数组,数组中每个元素都是一个PoolSubpage链表,PoolSubpage之间可以通过next,prev组成链表。

感兴趣的同学可以参考《内存对齐类SizeClasses》。

注意,Small内存size并不一定小于pageSize(默认为8K)

默认Small内存size <= 28672(28KB)

关于Normal内存块,Small内存块,pageSize,可参考《PoolChunk实现原理》。

PoolSubpage实际上就是PoolChunk中的一个Normal内存块,大小为其管理的内存块size与pageSize最小公倍数。

PoolSubpage使用位图的方式管理内存块。

PoolSubpage#bitmap是一个long数组,其中每个long元素上每个bit位都可以代表一个内存块是否使用。

内存分配

分配Small内存块有两个步骤

  1. PoolChunk中分配PoolSubpage。

    如果PoolArena#smallSubpagePools中已经有对应的PoolSubpage缓冲,则不需要该步骤。
  2. PoolSubpage上分配内存块

PoolChunk#allocateSubpage

private long allocateSubpage(int sizeIdx) {
// #1
PoolSubpage<T> head = arena.findSubpagePoolHead(sizeIdx);
synchronized (head) {
//allocate a new run
// #2
int runSize = calculateRunSize(sizeIdx);
//runSize must be multiples of pageSize
// #3
long runHandle = allocateRun(runSize);
if (runHandle < 0) {
return -1;
}
// #4
int runOffset = runOffset(runHandle);
int elemSize = arena.sizeIdx2size(sizeIdx); PoolSubpage<T> subpage = new PoolSubpage<T>(head, this, pageShifts, runOffset,
runSize(pageShifts, runHandle), elemSize); subpages[runOffset] = subpage;
// #5
return subpage.allocate();
}
}

#1 这里涉及修改PoolArena#smallSubpagePools中的PoolSubpage链表,需要同步操作

#2 计算内存块size和pageSize最小公倍数

#3 分配一个Normal内存块,作为PoolSubpage的底层内存块,大小为Small内存块size和pageSize最小公倍数

#4 构建PoolSubpage

runOffset,即Normal内存块偏移量,也是该PoolSubpage在整个Chunk中的偏移量

elemSize,Small内存块size

#5 在subpage上分配内存块

PoolSubpage(PoolSubpage<T> head, PoolChunk<T> chunk, int pageShifts, int runOffset, int runSize, int elemSize) {
// #1
this.chunk = chunk;
this.pageShifts = pageShifts;
this.runOffset = runOffset;
this.runSize = runSize;
this.elemSize = elemSize;
bitmap = new long[runSize >>> 6 + LOG2_QUANTUM]; // runSize / 64 / QUANTUM
init(head, elemSize);
} void init(PoolSubpage<T> head, int elemSize) {
doNotDestroy = true;
if (elemSize != 0) {
// #2
maxNumElems = numAvail = runSize / elemSize;
nextAvail = 0;
bitmapLength = maxNumElems >>> 6;
if ((maxNumElems & 63) != 0) {
bitmapLength ++;
} for (int i = 0; i < bitmapLength; i ++) {
bitmap[i] = 0;
}
}
// #3
addToPool(head);
}

#1 bitmap长度为runSize / 64 / QUANTUM,从《内存对齐类SizeClasses》可以看到,runSize都是2^LOG2_QUANTUM的倍数。

#2

elemSize:每个内存块的大小

maxNumElems:内存块数量

bitmapLength:bitmap使用的long元素个数,使用bitmap中一部分元素足以管理全部内存块。

(maxNumElems & 63) != 0,代表maxNumElems不能整除64,所以bitmapLength要加1,用于管理余下的内存块。

#3 添加到PoolSubpage链表中

前面分析《Netty内存池与PoolArena》中说过,在PoolArena中分配Small内存块时,首先会从PoolArena#smallSubpagePools中查找对应的PoolSubpage​。如果找到了,直接从该PoolSubpage​上分配内存。否则,分配一个Normal内存块,创建PoolSubpage​,再在上面分配内存块。

PoolSubpage#allocate

long allocate() {
// #1
if (numAvail == 0 || !doNotDestroy) {
return -1;
}
// #2
final int bitmapIdx = getNextAvail();
// #3
int q = bitmapIdx >>> 6;
int r = bitmapIdx & 63;
assert (bitmap[q] >>> r & 1) == 0;
bitmap[q] |= 1L << r;
// #4
if (-- numAvail == 0) {
removeFromPool();
}
// #5
return toHandle(bitmapIdx);
}

#1 没有可用内存块,分配失败。通常PoolSubpage分配完成后会从PoolArena#smallSubpagePools中移除,不再在该PoolSubpage上分配内存,所以一般不会出现这种场景。

#2 获取下一个可用内存块的bit下标

#3 设置对应bit为1,即已使用

bitmapIdx >>> 6,获取该内存块在bitmap数组中第q元素

bitmapIdx & 63,获取该内存块是bitmap数组中第q个元素的第r个bit位

bitmap[q] |= 1L << r,将bitmap数组中第q个元素的第r个bit位设置为1,表示已经使用

#4 所有内存块已分配了,则将其从PoolArena中移除。

#5 toHandle 转换为最终的handle

private int getNextAvail() {
int nextAvail = this.nextAvail;
if (nextAvail >= 0) {
this.nextAvail = -1;
return nextAvail;
}
return findNextAvail();
}

nextAvail为初始值或free时释放的值。

如果nextAvail存在,设置为不可用后直接返回该值。

如果不存在,调用findNextAvail查找下一个可用内存块。

private int findNextAvail() {
final long[] bitmap = this.bitmap;
final int bitmapLength = this.bitmapLength;
// #1
for (int i = 0; i < bitmapLength; i ++) {
long bits = bitmap[i];
if (~bits != 0) {
return findNextAvail0(i, bits);
}
}
return -1;
} private int findNextAvail0(int i, long bits) {
final int maxNumElems = this.maxNumElems;
final int baseVal = i << 6; // #2
for (int j = 0; j < 64; j ++) {
if ((bits & 1) == 0) {
int val = baseVal | j;
if (val < maxNumElems) {
return val;
} else {
break;
}
}
bits >>>= 1;
}
return -1;
}

#1 遍历bitmap,~bits != 0,表示存在一个bit位不为1,即存在可用内存块。

#2 遍历64个bit位,

(bits & 1) == 0,检查最低bit位是否为0(可用),为0则返回val。

val等于 (i << 6) | j,即i * 64 + j,该bit位在bitmap中是第几个bit位。

bits >>>= 1,右移一位,处理下一个bit位。

内存释放

释放Small内存块可能有两个步骤

  1. 释放PoolSubpage的上内存块
  2. 如果PoolSubpage中的内存块已全部释放,则从Chunk中释放该PoolSubpage,同时从PoolArena#smallSubpagePools移除它。

PoolSubpage#free

boolean free(PoolSubpage<T> head, int bitmapIdx) {
if (elemSize == 0) {
return true;
}
// #1
int q = bitmapIdx >>> 6;
int r = bitmapIdx & 63;
assert (bitmap[q] >>> r & 1) != 0;
bitmap[q] ^= 1L << r; setNextAvail(bitmapIdx);
// #2
if (numAvail ++ == 0) {
addToPool(head);
return true;
} // #3
if (numAvail != maxNumElems) {
return true;
} else {
// #4
if (prev == next) {
// Do not remove if this subpage is the only one left in the pool.
return true;
} // #5
doNotDestroy = false;
removeFromPool();
return false;
}
}

#1 将对应bit位设置为可以使用

#2 在PoolSubpage的内存块全部被使用时,释放了某个内存块,这时重新加入到PoolArena中。

#3 未完全释放,即还存在已分配内存块,返回true

#4 逻辑到这里,是处理所有内存块已经完全释放的场景。

PoolArena#smallSubpagePools链表组成双向链表,链表中只有head和当前PoolSubpage时,当前PoolSubpage的prev,next都指向head。

这时当前​PoolSubpage是PoolArena中该链表最后一个PoolSubpage,不释放该PoolSubpage,以便下次申请内存时直接从该PoolSubpage上分配。

#5 从PoolArena中移除,并返回false,这时PoolChunk会将释放对应Page节点。

void free(long handle, int normCapacity, ByteBuffer nioBuffer) {
if (isSubpage(handle)) {
// #1
int sizeIdx = arena.size2SizeIdx(normCapacity);
PoolSubpage<T> head = arena.findSubpagePoolHead(sizeIdx); PoolSubpage<T> subpage = subpages[runOffset(handle)];
assert subpage != null && subpage.doNotDestroy; synchronized (head) {
// #2
if (subpage.free(head, bitmapIdx(handle))) {
//the subpage is still used, do not free it
return;
}
}
} // #3
...
}

#1

查找head节点,同步

#2

调用subpage#free释放Small内存块

如果subpage#free返回false,将继续向下执行,这时会释放PoolSubpage整个内存块,否则,不释放PoolSubpage内存块。

#3 释放Normal内存块,就是释放PoolSubpage整个内存块。该部分内容可参考《PoolChunk实现原理》。

如果您觉得本文不错,欢迎关注我的微信公众号,系列文章持续更新中。您的关注是我坚持的动力!

Netty源码解析 -- PoolSubpage实现原理的更多相关文章

  1. Netty源码解析 -- PoolChunk实现原理

    本文主要分享Netty中PoolChunk如何管理内存. 源码分析基于Netty 4.1.52 内存管理算法 首先说明PoolChunk内存组织方式. PoolChunk的内存大小默认是16M,Net ...

  2. Netty源码解析 -- PoolChunk实现原理(jemalloc 3的算法)

    前面文章已经分享了Netty如何实现jemalloc 4算法管理内存. 本文主要分享Netty 4.1.52之前版本中,PoolChunk如何使用jemalloc 3算法管理内存. 感兴趣的同学可以对 ...

  3. 顺序线性表 ---- ArrayList 源码解析及实现原理分析

    原创播客,如需转载请注明出处.原文地址:http://www.cnblogs.com/crawl/p/7738888.html ------------------------------------ ...

  4. Netty源码解析—客户端启动

    Netty源码解析-客户端启动 Bootstrap示例 public final class EchoClient { static final boolean SSL = System.getPro ...

  5. Netty源码解析---服务端启动

    Netty源码解析---服务端启动 一个简单的服务端代码: public class SimpleServer { public static void main(String[] args) { N ...

  6. Netty 源码解析(三): Netty 的 Future 和 Promise

    今天是猿灯塔“365篇原创计划”第三篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源码解析(二): Netty 的 Channel 当前:Ne ...

  7. Netty 源码解析(九): connect 过程和 bind 过程分析

    原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 今天是猿灯塔“365篇原创计划”第九篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源 ...

  8. Netty 源码解析(八): 回到 Channel 的 register 操作

    原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 今天是猿灯塔“365篇原创计划”第八篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源 ...

  9. Netty 源码解析(七): NioEventLoop 工作流程

    原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 今天是猿灯塔“365篇原创计划”第七篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源 ...

随机推荐

  1. 基于chaosblade的故障注入平台小试

    当今社会互联网应用越来越广泛,用户量日益剧增.在人们对互联网服务的依赖性增大的同时,也对服务的可用性和体验感有了更高的要求.那么如何保障服务在运营过程中能一直给用户提供稳定的.不间断的.可靠可信的服务 ...

  2. 聊聊ReentrantLock实现原理

    ReentrantLock 是常用的锁,相对于Synchronized ,lock锁更人性化,阅读性更强 从LOCK切入 考虑下面的场景如果有A,B线程,同时去执行lock.lock(Lock loc ...

  3. 2. git命令行操作之本地库操作

    2.1 本地库初始化 git init 命令 用于创建一个空的Git本地仓库或重新初始化一个现有本地仓库 注:.git目录中存放的是本地库相关的子目录和文件,不要删除也不要随意修改 git confi ...

  4. 关于C语言编程的高效学习方法,首要任务是掌握高效编程,其次乃代码优化!

    在本篇文章中,我收集了很多经验和方法.应用这些经验和方法,可以帮助我们从执行速度和内存使用等方面来优化C语言代码. 简介 在最近的一个项目中,我们需要开发一个运行在移动设备上但不保证图像高质量的轻量级 ...

  5. zk与eureka区别

    cap永远的神!

  6. vscode 中 eslint prettier 和 eslint -loader 配置关系

    前置 本文将探究 vscode prettier 插件 和 eslint 插件在 vscode 中的配置以及这两者对应的在项目中的配置文件的关系,最后提及 vscode eslint 插件配置与 es ...

  7. Android中Application的意义及使用方法

    首先,在一个Android程序中,有且只有一个Application对象,在程序启动的时候,首先执行Application的onCreate方法,这是一个Android应用的入口,在开发中,我们常常自 ...

  8. CSDN-markdown编辑器使用方法

    这里写自定义目录标题 欢迎使用Markdown编辑器 新的改变 功能快捷键 合理的创建标题,有助于目录的生成 如何改变文本的样式 插入链接与图片 如何插入一段漂亮的代码片 生成一个适合你的列表 创建一 ...

  9. PyQt(Python+Qt)学习随笔:QListView的viewMode属性

    老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 QListView的viewMode属性用于控制QListView的视图模式,该属性类型为枚举类型Q ...

  10. PyQt(Python+Qt)学习随笔:QColumnView的resizeGripsVisible属性

    老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 QColumnView在一个视图中展示多个列表,每个下级列表是上一个列表的数据项的分支, QColu ...