Netty源码分析:PoolChunk
来源:互联网 发布:直播 知乎 编辑:程序博客网 时间:2024/05/16 12:02
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
- Netty学习之旅------源码分析Netty内存池分配机制初探--PoolArena、PoolChunk、PoolSubpage等数据结构分析
- netty PoolChunk内存分配一
- Netty 源码分析
- netty源码分析小结
- netty 源码分析一
- netty 源码分析二
- netty源码分析
- Netty源码分析
- netty源码分析一
- 【Netty】源码分析目录
- Netty源码分析:NioEventLoopGroup
- Netty源码分析:ServerBootstrap
- Netty源码分析:ChannelPipeline
- netty源码分析
- Netty源码分析:AbstractByteBuf
- Netty源码分析:PoolSubpage
- Netty源码分析:PoolArena
- 自制的分割线+应用
- 网络最大流-EdmondsKarp
- poj1066
- oracle常用技能整理(一)常用SQL
- TabLayout横滑菜单+viewpager+vtab
- Netty源码分析:PoolChunk
- maven常见问题总结
- maven整合ssm时mybatis-Spring的版本问题
- web 应用 为啥 需要用到 tomcat 之类的 部署
- 类加载过程
- 嵌入式软件开发QT-01-helloworld工程的几种编写方式以及详解
- ubuntu server 14.04下LAMP环境搭建
- Android ADB
- 类与对象以及面向对象的概念