Netty内存分配的思想

766 阅读5分钟

Netty内存分配的最核心思想是,将现有内存最大化利用,减少内存碎片。

Netty的内存分配是以PoolArena.allocate()方法作为入口的。

首先,通过normalizeCapacity()方法进行内存规范化,而当所需分配的内存小于512字节时,规范为大于当前值的第一个16字节的倍数,当所需分配的内存大于等于512字节且小于16MB时,规范为大于当前值的第一个2的指数幂,若大于16MB,则直接进行内存对齐。

举个例子:当所需内存为26字节时,会将内存分配为32字节。当所需内存为618字节时,会将内存分配为1024字节,

final int normCapacity = normalizeCapacity(reqCapacity); 

然后,根据内存的使用大小进行判断,是否小于8KB

if (isTinyOrSmall(normCapacity)) { // capacity < pageSize 

接下来,通过PoolSubpage数组维护小于8KB内存的分配,而PoolSubpage数组又会分为两类:

tinySubpagePools数组,用于分配管理小于512字节的内存,默认长度为32,数组元素为可以组成双向链表的PoolSubpage,每个PoolSubpage所管理的总内存大小都是8KB,数组下标为0的元素没被使用,下标为1的PoolSubpage用于分配管理16字节的内存,8kb/16b = 512,即会有512个内存块。数组的下标每加1,管理的内存大小会加16字节。以此类推,下标为2的PoolSubpage用于分配管理32字节的内存,会有256个内存块。PoolSubpage内部用bitmap来记录内存块的使用情况。

整体结构如下图:

image.png

smallSubpagePools数组:用于分配大于等于512字节的内存,默认长度为4。

数组元素为可以组成双向链表的PoolSubpage,每个PoolSubpage所管理的总内存大小都是8kb,下标为0的PoolSubpage用于分配管理512字节的内存,8kb/512b = 16,即会有16个内存块。数组的下标每加1,管理的内存大小会翻倍。以此类推,下标为1的PoolSubpage用于分配管理1KB的内存,会有8个内存块。PoolSubpage内部用bitmap来记录内存块的使用情况。

整体结构如下图:

image.png

小于8KB的内存分配分支走完了,接下来我们看下,大于等于8KB的内存分配分支怎么走。

依旧是一个判断语句,判断待分配的内存是否大于等于chunkSize,即16MB:

if (normCapacity <= chunkSize) {

接下来就是进行小于16MB的内存分配,代码片段截取如下:

image.png

那么,q050、q025、q000、qInit和q075到底是什么呢?

private final PoolChunkList<T> q050;
private final PoolChunkList<T> q025;
private final PoolChunkList<T> q000;
private final PoolChunkList<T> qInit;
private final PoolChunkList<T> q075;
private final PoolChunkList<T> q100;

PoolChunkList集合里面的PoolChunk又是什么呢?

Netty底层的内存分配和回收管理就是由PoolChunk实现的,默认申请的内存大小是16MB,包括刚才讲的PoolSubpage的实现原理,其实也就是把PoolChunk下8KB的page节点划分为更小的内存段而已。当然,PoolChunk也负责大于等于8KB的内存分配。

在PoolChunk类里面,用depthMap和memoryMap两个数组来表示二叉树,两个数组的长度都是4096,其中depthMap存的是二叉树高度,memoryMap存的是内存分配情况,有意思的是,memoryMap树的父节点内存,同时也被它的子节点共享。树的根节点内存大小为16M,两个子节点各是8M,再往下一层的四个子节点是各是4M,以此类推,树的第11层的是2048个8K的叶子节点,也就是page。

如图所示:

image.png

初始化时,depthMap和memoryMap元素的值完全相等,举个例子:

depthMap[1025] = memoryMap [1025] = 10

但不同的是,depthMap的值初始化后不再改变,但memoryMap的值是随着会随着节点分配而进行变更,不仅仅自己的节点变更,也会更新祖先节点的值。

举个例子:

如果分配16KB的内存,在第10层去寻找一个可用节点,假设memoryMap [1025] = 10,代表其本身和下面的所有子节点都可以被分配,将它的值从10改为12(数的最大高度是11),表示不可用。同时,会根据情况级联更新其祖先节点的值。

如果memoryMap [1025] = 11,表示本身不可以被分配了,但是在第11层,有空闲的节点可以被分配,这种情况,会继续在第10层需要其他可用节点。

如果memoryMap [1025] = 12,表示其本身和下面的所有子节点全部占满,都不可以被分配,这种情况,会继续在第10层需要其他可用节点。

我们再来讲一下PoolChunkList,它负责管理PoolChunk的生命周期,将多个PoolChunk维护成一个双向链表,PoolChunkList之间也是一个双向链表。

如图所示:

image.png

PoolChunk的生命周期不会与某个PoolChunkList完全绑定,而是会随着内存的分配和释放,在各个PoolChunkList中前后移动。

如果PoolChunk通过allocate()方法进行内存分配后,其内存使用率也会随着上升,内存使用量超过了maxUsage,就会从当前的PoolChunkList移动到后一个PoolChunkList中。

如果PoolChunk通过free()方法进行内存释放后,其内存使用率也会随着下降,当内存使用量低于minUsage,就会从当前的PoolChunkList移动到前一个PoolChunkList中。

我们回到前面的问题,q050、q025、q000、qInit和q075到底是什么呢?

qInit:内存使用率在0-25%之间的PoolChunk所组成的PoolChunkList

q000:内存使用率在1-50%之间的PoolChunk所组成的PoolChunkList

q025:内存使用率在25-75%之间的PoolChunk所组成的PoolChunkList

q050:内存使用率在50-100%之间的PoolChunk所组成的PoolChunkList

q075:内存使用率在75-100%之间的PoolChunk所组成的PoolChunkList

q100:内存使用率等于100%的PoolChunk所组成的PoolChunkList

那么,疑问来了,为什么按照这种顺序进行内存分配呢?

image.png

这是一种折中和取舍的方案,我们既希望内存分配的成功率高一些,又希望整体的内存利用率高一些,保证相对空闲的内存能够尽可能地被回收释放。

因此,先从q050的PoolChunkList进行内存分配,这样可以让内存的利用率在一个较高的水平,又不会让内存分配的成功率太低。如果q050内存分配失败了,则更倾向于优先保证内存分配的成功率,于是按照q025—>q000—>qInit的顺序进行内存分配,把q075放在最后,因为这个PoolChunkList的内存分配成功率太低了。

接下来我们看最后一个逻辑分支,就是大于16MB的内存分配。

对于大内存的分配处理最简单,直接通过allocateHuge()方法,按照所需要的内存大小去申请并使用即可。

全文完。