本文主要分享Netty中PoolChunk如何管理内存。

源码分析基于Netty 4.1.52

内存管理算法

首先说明PoolChunk内存组织方式。

PoolChunk的内存大小默认是16M,Netty将它划分为2048个page,每个page为8K。

PoolChunk上可以分配Normal内存块。

Normal内存块大小必须是page的倍数。

PoolChunk通过runsAvail字段管理内存块。

PoolChunk#runsAvail是PriorityQueue数组,其中PriorityQueue存放的是handle。

handle可以理解为一个句柄,维护一个内存块的信息,由以下部分组成

  • o: runOffset ,在chunk中page偏移索引,从0开始,15bit
  • s: size,当前位置可分配的page数量,15bit
  • u: isUsed,是否使用?, 1bit
  • e: isSubpage,是否在subpage中, 1bit
  • b: bitmapIdx,内存块在subpage中的索引,不在subpage则为0, 32bit

前面《内存对齐类SizeClasses》文章说过,SizeClasses将sizeClasses表格中isMultipageSize为1的行取出可以组成一个新表格,这里称为Page表格

runsAvail数组默认长度为40,每个位置index上放的handle代表了存在一个可用内存块,并且可分配pageSize大于等于(pageIdx=index)上的pageSize,小于(pageIdex=index+1)的pageSize。

如runsAvail[11]上的handle的size可分配pageSize可能为16 ~ 19,

假如runsAvail[11]上handle的size为18,如果该handle分配了7个page,剩下的11个page,这时要将handle移动runsAvail[8](当然,handle的信息要调整)。

这时如果要找分配6个page,就可以从runsAvail[5]开始查找runsAvail数组,如果前面runsAvail[5]~runsAvail[7]都没有handle,就找到了runsAvail[8]。

分配6个page之后,剩下的5个page,handle移动runsAvail[4]。

先看一下PoolChunk的构造函数

PoolChunk(PoolArena<T> arena, T memory, int pageSize, int pageShifts, int chunkSize, int maxPageIdx, int offset) {
// #1
unpooled = false;
this.arena = arena;
this.memory = memory;
this.pageSize = pageSize;
this.pageShifts = pageShifts;
this.chunkSize = chunkSize;
this.offset = offset;
freeBytes = chunkSize; runsAvail = newRunsAvailqueueArray(maxPageIdx);
runsAvailMap = new IntObjectHashMap<Long>();
subpages = new PoolSubpage[chunkSize >> pageShifts]; // #2
int pages = chunkSize >> pageShifts;
long initHandle = (long) pages << SIZE_SHIFT;
insertAvailRun(0, pages, initHandle); cachedNioBuffers = new ArrayDeque<ByteBuffer>(8);
}

#1

unpooled: 是否使用内存池

arena:该PoolChunk所属的PoolArena

memory:底层的内存块,对于堆内存,它是一个byte数组,对于直接内存,它是(jvm)ByteBuffer,但无论是哪种形式,其内存大小默认都是16M。

pageSize:page大小,默认为8K。

chunkSize:整个PoolChunk的内存大小,默认为16777216,即16M。

offset:底层内存对齐偏移量,默认为0。

runsAvail:初始化runsAvail

runsAvailMap:记录了每个内存块开始位置和结束位置的runOffset和handle映射。

#2 insertAvailRun方法在runsAvail数组最后位置插入一个handle,该handle代表page偏移位置为0的地方可以分配16M的内存块

内存分配

PoolChunk#allocate

boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int sizeIdx, PoolThreadCache cache) {
final long handle;
// #1
if (sizeIdx <= arena.smallMaxSizeIdx) {
// small
handle = allocateSubpage(sizeIdx);
if (handle < 0) {
return false;
}
assert isSubpage(handle);
} else {
// #2
int runSize = arena.sizeIdx2size(sizeIdx);
handle = allocateRun(runSize);
if (handle < 0) {
return false;
}
} // #3
ByteBuffer nioBuffer = cachedNioBuffers != null? cachedNioBuffers.pollLast() : null;
initBuf(buf, nioBuffer, handle, reqCapacity, cache);
return true;
}

#1 处理Small内存块申请,调用allocateSubpage方法处理,后续文章解析。

#2 处理Normal内存块申请

sizeIdx2size方法根据内存块索引查找对应内存块size。sizeIdx2size是PoolArena父类SizeClasses提供的方法,可参考系列文章《内存对齐类SizeClasses》。

allocateRun方法负责分配Normal内存块,返回handle存储了分配的内存块大小和偏移量。

#3 使用handle和底层内存类(ByteBuffer)初始化ByteBuf了。

private long allocateRun(int runSize) {
// #1
int pages = runSize >> pageShifts;
// #2
int pageIdx = arena.pages2pageIdx(pages); synchronized (runsAvail) {
//find first queue which has at least one big enough run
// #3
int queueIdx = runFirstBestFit(pageIdx);
if (queueIdx == -1) {
return -1;
} //get run with min offset in this queue
PriorityQueue<Long> queue = runsAvail[queueIdx];
long handle = queue.poll(); assert !isUsed(handle);
// #4
removeAvailRun(queue, handle);
// #5
if (handle != -1) {
handle = splitLargeRun(handle, pages);
}
// #6
freeBytes -= runSize(pageShifts, handle);
return handle;
}
}

#1 计算所需的page数量

#2 计算对应的pageIdx

注意,pages2pageIdx方法会将申请内存大小对齐为上述Page表格中的一个size。例如申请172032字节(21个page)的内存块,pages2pageIdx方法计算结果为13,实际分配196608(24个page)的内存块。

#3 从pageIdx开始遍历runsAvail,找到第一个handle。

该handle上可以分配所需内存块。

#4 从runsAvail,runsAvailMap移除该handle信息

#5#3步骤找到的handle上划分出所要的内存块。

#6 减少可用内存字节数

private long splitLargeRun(long handle, int needPages) {
assert needPages > 0; // #1
int totalPages = runPages(handle);
assert needPages <= totalPages; int remPages = totalPages - needPages; // #2
if (remPages > 0) {
int runOffset = runOffset(handle); // keep track of trailing unused pages for later use
int availOffset = runOffset + needPages;
long availRun = toRunHandle(availOffset, remPages, 0);
insertAvailRun(availOffset, remPages, availRun); // not avail
return toRunHandle(runOffset, needPages, 1);
} //mark it as used
handle |= 1L << IS_USED_SHIFT;
return handle;
}

#1 totalPages,从handle中获取当前位置可用page数。

remPages,分配后剩余page数。

#2 剩余page数大于0

availOffset,计算剩余page开始偏移量

生成一个新的handle,availRun

insertAvailRun将availRun插入到runsAvail,runsAvailMap中

内存释放

void free(long handle, int normCapacity, ByteBuffer nioBuffer) {
... // #1
int pages = runPages(handle); synchronized (runsAvail) {
// collapse continuous runs, successfully collapsed runs
// will be removed from runsAvail and runsAvailMap
// #2
long finalRun = collapseRuns(handle); // #3
finalRun &= ~(1L << IS_USED_SHIFT);
//if it is a subpage, set it to run
finalRun &= ~(1L << IS_SUBPAGE_SHIFT);
insertAvailRun(runOffset(finalRun), runPages(finalRun), finalRun);
freeBytes += pages << pageShifts;
} if (nioBuffer != null && cachedNioBuffers != null &&
cachedNioBuffers.size() < PooledByteBufAllocator.DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK) {
cachedNioBuffers.offer(nioBuffer);
}
}

#1 计算释放的page数

#2 如果可以,将前后的可用内存块进行合并

#3 插入新的handle

collapseRuns

private long collapseRuns(long handle) {
return collapseNext(collapsePast(handle));
}

collapsePast方法合并前面的可用内存块

collapseNext方法合并后面的可用内存块

private long collapseNext(long handle) {
for (;;) {
// #1
int runOffset = runOffset(handle);
int runPages = runPages(handle); Long nextRun = getAvailRunByOffset(runOffset + runPages);
if (nextRun == null) {
return handle;
} int nextOffset = runOffset(nextRun);
int nextPages = runPages(nextRun); //is continuous
// #2
if (nextRun != handle && runOffset + runPages == nextOffset) {
//remove next run
removeAvailRun(nextRun);
handle = toRunHandle(runOffset, runPages + nextPages, 0);
} else {
return handle;
}
}
}

#1 getAvailRunByOffset方法从runsAvailMap中找到下一个内存块的handle。

#2 如果是连续的内存块,则移除下一个内存块handle,并将其page合并生成一个新的handle。

下面来看一个例子



大家可以结合例子中runsAvail和内存使用情况的变化,理解上面的代码。

实际上,2个Page的内存块是通过Subpage分配,回收时会放回线程缓存中而不是直接释放存块,但为了展示PoolChunk中内存管理过程,图中不考虑这些场景。

PoolChunk在Netty 4.1.52版本修改了算法,引入了jemalloc 4的算法 -- https://github.com/netty/netty/commit/0d701d7c3c51263a1eef56d5a549ef2075b9aa9e#diff-6850686cf7ebc7b9ddb873389ded45ebf40e6c1ccf411c44b744e7d3ca2ff774

Netty 4.1.52之前的版本,PoolChunk引入的是jemalloc 3的算法,使用二叉树管理内存块。有兴趣的同学可以参考我后续的文章《PoolChunk实现(jemalloc 3的算法)》

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

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

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

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

  2. Netty源码解析 -- PoolSubpage实现原理

    前面文章说了PoolChunk如何管理Normal内存块,本文分享PoolSubpage如何管理Small内存块. 源码分析基于Netty 4.1.52 内存管理算法 PoolSubpage负责管理S ...

  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. C#两行代码实现三维地球

    一.             为什么要用三维地球? 三维地球是地理信息技术的一个重要发展方向,相比较二维地图技术,三维地球最大的特点是更直观更形象地表达地理信息和空间上的方位.我们可以在三维气象模拟. ...

  2. 内网渗透 day14-empire基础命令的使用

    empire的基础操作 目录 1. 建立监听器 2. 设置stagers 3. 用户交互 4. 提权 1. 建立监听器 help    查看帮助命令 listeners     查看监听器 useli ...

  3. Flink基础:时间和水印

    ​ 往期推荐: Flink基础:入门介绍 Flink基础:DataStream API Flink基础:实时处理管道与ETL Flink深入浅出:资源管理 Flink深入浅出:部署模式 Flink深入 ...

  4. Python_环境搭建_jupyterNotebook的使用

    # @ Author : Collin_PXY # 虚拟环境的创建及Jupyter Notebook的基本使用 # Anaconda 和 Jupter Notebook的使用: # 首先得需要安装 A ...

  5. 【应用服务 App Service】App Service 新手资料包

    问题描述 云计算的趋势已成定局,作为一个开发者,如果想对PaaS服务中的应用服务有一个初步的了解,从那些资料入手呢? 以Azure的官方文档作为基础库,从中选择出部分内容,分为:本地开发工具,App ...

  6. 【django】长轮询

    API.PY import queue from django.contrib.auth.hashers import check_password from rest_framework.views ...

  7. Databricks说的Lakehouse是什么?

    在过去的几年里,Lakehouse作为一种新的数据管理范式,已独立出现在Databricks的许多用户和应用案例中.在这篇文章中,我们将阐述这种新范式以及它相对于之前方案的优势. 数据仓库在决策支持和 ...

  8. 双汇大数据方案选型:从棘手的InfluxDB+Redis到毫秒级查询的TDengine

    双汇发展多个分厂的能源管控大数据系统主要采用两种技术栈:InfluxDB/Redis和Kafka/Redis/HBase/Flink,对于中小型研发团队来讲,无论是系统搭建,还是实施运维都非常棘手.经 ...

  9. create-react-app添加对TypeScript支持

    背景 最近一直在重构react项目,由于项目历史原因,将之前parcel打包工具换成了webpack,并选择了使用create-react-app作为项目开发脚手架. 接着就是把项目中flow类型检查 ...

  10. 【python爬虫】用requests库模拟登陆人人网

    说明:以前是selenium登陆取cookie的方法比较复杂,改用这个 """ 用requests库模拟登陆人人网 """ import r ...