本文主要分享Netty中PoolChunk如何管理内存。
源码分析基于Netty 4.1.52git
首先说明PoolChunk内存组织方式。
PoolChunk的内存大小默认是16M,Netty将它划分为2048个page,每一个page为8K。
PoolChunk上能够分配Normal内存块。
Normal内存块大小必须是page的倍数。github
PoolChunk经过runsAvail字段管理内存块。
PoolChunk#runsAvail是PriorityQueue
handle能够理解为一个句柄,维护一个内存块的信息,由如下部分组成
前面《内存对齐类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映射。jvm
#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存储了分配的内存块大小和偏移量。this
#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的算法)》
若是您以为本文不错,欢迎关注个人微信公众号,系列文章持续更新中。您的关注是我坚持的动力!