Netty源码分析:PoolChunk
Posted HelloWorld_EE
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Netty源码分析:PoolChunk相关的知识,希望对你有一定的参考价值。
Netty源码分析:PoolChunk
Chunk主要用来组织和管理多个Page的内存分配和释放。在Netty中,Chunk中的Page被构建成一颗二叉树。本博文将从源码的角度来看下PoolChunk。
1、属性和构造函数
先看下PoolChunk的属性和构造函数
final class PoolChunk<T> // PoolChunk会涉及到具体的内存,泛型T表示byte[](堆内存)、或java.nio.ByteBuffer(堆外内存)
final PoolArena<T> arena;//表示该PoolChunk所属的PoolArena。
final T memory;// 具体用来表示内存;byte[]或java.nio.ByteBuffer。
final boolean unpooled;// 是否是可重用的,unpooled=false表示可重用
private final byte[] memoryMap;
private final byte[] depthMap;
private final PoolSubpage<T>[] subpages;//表示该PoolChunk所包含的PoolSubpage。也就是PoolChunk连续的可用内存。
/** Used to determine if the requested capacity is equal to or greater than pageSize. */
private final int subpageOverflowMask;
private final int pageSize;//每个PoolSubpage的大小,默认为8192个字节(8K)
private final int pageShifts;
private final int maxOrder;
private final int chunkSize;
private final int log2ChunkSize;
private final int maxSubpageAllocs;
/** Used to mark memory as unusable */
private final byte unusable;
private int freeBytes; //当前PoolChunk空闲的内存。
PoolChunkList<T> parent;//一个PoolChunk分配后,会根据使用率挂在PoolArena的一个PoolChunkList中
// PoolChunk本身设计为一个链表结构
PoolChunk<T> prev;
PoolChunk<T> next;
PoolChunk(PoolArena<T> arena, T memory, int pageSize, int maxOrder, int pageShifts, int chunkSize)
unpooled = false;
this.arena = arena;
this.memory = memory;
this.pageSize = pageSize;
this.pageShifts = pageShifts;
this.maxOrder = maxOrder;
this.chunkSize = chunkSize;
unusable = (byte) (maxOrder + 1);
log2ChunkSize = log2(chunkSize);
subpageOverflowMask = ~(pageSize - 1);
freeBytes = chunkSize;
assert maxOrder < 30 : "maxOrder should be < 30, but is: " + maxOrder;
maxSubpageAllocs = 1 << maxOrder;
// Generate the memory map.
memoryMap = new byte[maxSubpageAllocs << 1];
depthMap = new byte[memoryMap.length];
int memoryMapIndex = 1;
for (int d = 0; d <= maxOrder; ++ d) // move down the tree one level at a time
int depth = 1 << d;
for (int p = 0; p < depth; ++ p)
// in each level traverse left to right and set value to the depth of subtree
memoryMap[memoryMapIndex] = (byte) d;
depthMap[memoryMapIndex] = (byte) d;
memoryMapIndex ++;
subpages = newSubpageArray(maxSubpageAllocs);
PoolChunk默认情况下:maxOrder = 11,即根据maxSubpageAllocs = 1 << maxOrder
可得一个PoolChunk默认情况下由2^11=2048个SubPage构成,而默认情况下一个page默认大小为8k,即pageSize=8K。
重点来看下memoryMap这个字段,PoolChunk中所有的PoolSubpage都放在PoolSubpage[] subpages中,为了更好的分配,Netty用一颗平衡二叉树记录每个PoolSubpage的分配情况。假设PoolChunk由16个PoolSubpage构成(为便于分析,这里就不用默认的2048个page来进行说明chunk的结构了),那么这些PoolSubpage将会按照如下的结构组织起来。
看上面的构造函数中的两层for循环可以得到:从树根到树叶节点按每层将节点所在的(层数)依次保存在memoryMap中,即memoryMap数组中每个位置保存的是该节点所在的层数。如下就是示例的结果。
memoryMap=第0位没用到,0,1,1,2,2,2,2,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,//memoryMap数组元素长度为 (1<<maxOrder)>>1=32
memoryMap存储二叉树每个节点所在的层数有什么作用呢?
对于下图“圈出来”的节点,在memoryMap的索引为4,其层数是2,则:
1、如果memoryMap[4] = 2,则表示其本身到下面所有的子节点都可以被分配;
2、如果memoryMap[4] = 3, 则表示该节点下有子节点已经分配过,则该节点不能直接被分配,而其子节点中的第3层还存在未分配的节点;例如:当我们请求一个大小为4K存储区域时就会出现这种情况
3、如果memoryMap[4] = 4,则表示该节点下的2个子节点已经被分配过(但是还存在某个子节点没有分完),则该节点和两个子节点不能直接被分配,而其子节点中的第4层还存在未分配的节点。例如:当我们申请一个大小为8K和4K的存储区域是就会出现这种情况
3、如果memoryMap[4] = 5 (即总层数 + 1), 可分配的深度已经大于总层数, 则表示该节点下的所有子节点都已经被分配。例如:当我们申请一个大小为16K的存储区域时就会出现这种情况
可以这么说:如果memoryMap[i] = maxOrder+1,就表示该位置的PoolSubpage已被分配完,如果memoryMap[i] < maxOrder+1,则说明还可以分配,具体还可以分配多少,就和memoryMap[i]以及其所有子节点的值、pageSize有关。
这里假设一个PoolSubpage的大小为4K,如果申请一个大小为1K的存储区域时,该什么办呢?
对于小于一个Page的内存,Netty在Page中完成分配。每个Page会被切分成大小相同的多个存储块,存储块的大小由第一次申请的内存块大小决定。对于Page的大小为4K,第一次申请的时1K,则这个Page就会被分成4个存储块。
一个Page只能用于分配与第一次申请时大小相同的内存,例如,一个4K的Page,如果第一次分配了1K的内存,那么后面这个Page就只能继续分配1K的内存,如果有一个申请2K内存的请求,就需要在一个新的Page中进行分配。
Page中存储区域的使用状态通过一个long数组来维护,数组中每个long的每一位表示一个块存储区域的占用情况:0表示未占用,1表示占用。例如:对于一个4K的Page来说如果这个Page用来分配1K的存储与区,那么long数组中就只有一个long类型的元素且这个数值的低4危用来指示4个存储区域的占用情况。
下面看看如何向PoolChunk申请一块内存区域,allocate函数的代码如下;
long allocate(int normCapacity)
if ((normCapacity & subpageOverflowMask) != 0) // >= pageSize
return allocateRun(normCapacity);
else
return allocateSubpage(normCapacity);//分析
从上面的函数可以看到根据用户申请的内存的大小,chunk采用了不同的方式,具体如下:
1、当需要分配的内存大于等于pageSize时,通过调用allocateRun函数实现内存分配。
2、当需要分配的内存小于pageSize时,通过调用allocateSubpage函数实现内存分配。
下面将会这两个函数进行分析。
1、allocateSubpage(normCapacity)
先看 allocateSubpage方法,代码如下:
private long allocateSubpage(int normCapacity)
int d = maxOrder; // subpages are only be allocated from pages i.e., leaves
int id = allocateNode(d);//找到符合要求的节点的索引。
if (id < 0)
return id;
final PoolSubpage<T>[] subpages = this.subpages;
final int pageSize = this.pageSize;
freeBytes -= pageSize;//修改该chunk的空闲内存大小
int subpageIdx = subpageIdx(id);//求余,得到page在subPages中的索引。
PoolSubpage<T> subpage = subpages[subpageIdx];
if (subpage == null)
subpage = new PoolSubpage<T>(this, id, runOffset(id), pageSize, normCapacity);
subpages[subpageIdx] = subpage;
else
subpage.init(normCapacity);
return subpage.allocate();
前面提到过,当需要分配的内存小于pageSize时 ,会把一个page分割成多段,进行内存分配。因此,第一步就是要找到一个符合要求的节点。该功能由如下的allocateNode函数来完成。
private int allocateNode(int d)
int id = 1;
int initial = - (1 << d); // has last d bits = 0 and rest all = 1
byte val = value(id);
if (val > d) // unusable
return -1;
while (val < d || (id & initial) == 0) // id & initial == 1 << d for all ids at depth d, for < d it is 0
id <<= 1;
val = value(id);
if (val > d) //当前节点不符合要求,需要到兄弟节点来寻找。
id ^= 1;//通过异或可以找到兄弟节点
val = value(id);
byte value = value(id);
assert value == d && (id & initial) == 1 << d : String.format("val = %d, id & initial = %d, d = %d",
value, id & initial, d);
setValue(id, unusable); // mark as unusable
updateParentsAlloc(id);
return id;
简单来说:该函数用于在二叉树的第d层寻找一个空闲page节点,返回的是该空闲Page节点在memoryMap的索引。 此时这里的 d = maxOrder。 代码的具体实现思路如下:
1、从根节点开始遍历,如果当前节点的val > d,说明存在子节点已经被分配了且剩余节点的内存大小不够,则此时需要到兄弟节点上继续查找。如果当前节点为根节点,根节点无兄弟节点,直接返回-1表示在该chunk不符合要求不能分配这么大的内存。
2、如果当前节点的val < d,说明该节点的内存可以被分配,则通过 id <<= 1 匹配下一层直到找到value(id) = d 且id在第d层的节点;
3、分配成功的节点需要标记为不可用,防止被再次分配,在memoryMap对应位置进行更新;
4、分配节点完成后,其父节点的状态也需要更新,并可能引起更上一层父节点的更新。父节点的val=min子节点val;
private void updateParentsAlloc(int id)
while (id > 1)
int parentId = id >>> 1;
byte val1 = value(id);
byte val2 = value(id ^ 1);
byte val = val1 < val2 ? val1 : val2;
setValue(parentId, val);
id = parentId;
看一个例子来说明allocateNode函数中寻找节点的算法的整个过程。
首先假设PoolChunnk由16个PoolSubpage构成,每个Page的大小为4K,那么这些PoolSubpage将会按照如下的结构组织起来。图中标出的数字为:当前节点在memoryMap中的所对应的val。
现假设用户申请一个4K的存储空间,则会将如下图所示用“红椭圆”标示的page分配出去。并进行了标识,修改了相应节点在memoryMap的值。
现在假设用户又申请一个4K的存储空间,则具体的流经过程如下图描述所示。最后的分配结果用“蓝色椭圆”进行了标识。
分配之后,修改相应节点在memoryMap的值的结果如下图所示。
以上几个图就可以很好的了解allocateNode函数中寻找节点的算法了。
回到allocateSubpage方法,在找到节点之后,即找到subpage之后由于申请的内存区域小于pageSize因此就开始调用subpage.allocate()来进行内存分配。具体细节将会在下篇博文讲解PoolSubpage的时候进行介绍,这里不进行介绍。
2、allocateRun(int normCapacity)
当需要分配的内存大于等于pageSize时,通过调用allocateRun函数实现内存分配。
/**
* Allocate a run of pages (>=1)
*
* @param normCapacity normalized capacity
* @return index in memoryMap
*/
private long allocateRun(int normCapacity)
int d = maxOrder - (log2(normCapacity) - pageShifts);//
int id = allocateNode(d);
if (id < 0)
return id;
freeBytes -= runLength(id);
return id;
该函数相比上面介绍的allocateSubpage方法类似且要简单,这个函数主要是利用allocateNode方法来寻找符合要求的节点即可,allocateNode方法在上面有详细的介绍。
这里主要理解下 int d = maxOrder - (log2(normCapacity) - pageShifts);
代码。这行代码的作用:根据normCapacity确定需要在二叉树的d层开始节点匹配。
还是以下面这个chunk为例来进行说明。
假设PoolChunnk由16个PoolSubpage构成,则由(maxSubpageAllocs = 1 << maxOrder;)可以得到maxOrder=log(16)=4,而根据每个Page的大小为4K可以得到pageShifts=12。因此假设用户申请一个大小为16K的缓存,则maxOrder - (log2(normCapacity) - pageShifts)=2,如下图“红圈”所示的那一层。
小结
看完PoolChunk,我们需要知道的东西就两点:
1、PoolChunk是通过二叉树的形式来组织Page的。
2、当用户申请内存时,无论是大于等于PageSize的还是小于PageSize的第一步都是通过allocateNode方法来寻找符合要求的节点。其中小于pageSize的内存分配是在PoolSubpage上来进行分配的。
参考资料
1、http://www.jianshu.com/p/c4bd37a3555b
2、http://blog.csdn.net/prestigeding/article/details/54598967
3、《Netty权威指南》
以上是关于Netty源码分析:PoolChunk的主要内容,如果未能解决你的问题,请参考以下文章