首先先来了解一对概念,堆内存和堆外内存。那么我们知道jvm其实是作为一个software跑在操作系统上的,而jvm首先会从操作系统中划出一块内存自己来管理,这块内存在jvm的管理下分成了堆,栈,计数器,方法区等等。在io操作的时候,无法直接将数据写入堆内存中,因为对于io操作,在linux下,只能由网络设备等硬件上读到内核内存中,再从堆外的内存写进堆内。为什么不能直接写入jvm的堆内存内,因为你要提供一个地址,而堆内存是会被回收的啊,鬼知道这个地址上是不是有什么重要的东西会被覆盖掉呢。那么这个方法比较垃的就是需要复制两次,必然影响效率,那么一定有人想到了,为什么不直接操作堆内核内存呢?对,没错,堆外内存就是这么产生的,也是这么用的,io直接读取堆外的内存,也就少去了复制的过程,清爽多了。但是新的问题又随之产生,堆外内存是不受jvm控制的,所以它的malloc和free都是需要手动完成的,这个过程实际上又是耗时的,所以,netty在处理的过程中,便最大化的复用了堆外内存,防止频繁的回收。
对,这一篇博客主要就是来学习netty对堆外内存的池化处理。这个过程是比较复杂的,首先先来看一张结构图(本来是想直接盗用一篇文章的图,结果发现这个大兄弟理解和我不太一致,就照着复制了一份比较丑的)
可以看到netty的内存池整个体系还是稍微有些庞大的,首先它的等级关系如图所示 arena > poolChunkList > poolChunk > subPage > element(最后一个数据结构没有,只是在某些情况下subPage会被划分成多块)。这里几个数据结构之间的耦合性还是蛮强的,所以怎么讲都不好理解,所以我就随便选择一种,从小到大的来看。
首先来看PoolSubpage,来看重要变量
private final int pageSize;
int elemSize;
private int maxNumElems;
private final long[] bitmap;
这个pageSize便是当前page的大小,默认为8k。而elemSize是用来将subpage分成更小的多个结构(也就是图中的element)的,elemSize决定一个element的大小和当前page一共有多少个element,即maxNumElems。 因为netty对数据大小做了处理,数据大小为16byte * 2的n次方,所以不会出现类似6k这样的数据,让page无法划分。
bitmap是用来标识page中element使用情况的,
bitmap = new long[pageSize >>> 10]; // pageSize / 16 / 64
首先,page中element的数量是不会多于 pageSize/16个的,而每个long有64位,每位便可标识一个element的使用情况,所以,用这么一个数组便可表示所有的element的使用情况。
再看一下它的init方法
void init(PoolSubpage<T> head, int elemSize) { doNotDestroy = true; this.elemSize = elemSize; if (elemSize != 0) { maxNumElems = numAvail = pageSize / elemSize; // 计算最大element和可用element nextAvail = 0; //下一个可用element bitmapLength = maxNumElems >>> 6; if ((maxNumElems & 63) != 0) { //这里保证bitmap中至少有一个long (maxNumElems < 64 ) bitmapLength ++; } for (int i = 0; i < bitmapLength; i ++) { bitmap[i] = 0; } } addToPool(head); }
最后把它加入池子中,那这个池子实际上是在poolArena中维护的链表,我们的图中也可以看到,这个随后再说。allocate过程如下
long allocate() { if (elemSize == 0) { return toHandle(0); } if (numAvail == 0 || !doNotDestroy) { return -1; } final int bitmapIdx = getNextAvail(); //获取bitmap中维护的下一个可用element int q = bitmapIdx >>> 6; //获取bitmap的下标 int r = bitmapIdx & 63; //获取在long中左偏移的位置 assert (bitmap[q] >>> r & 1) == 0; bitmap[q] |= 1L << r; // 如果这个page已经没有空闲,则把它移除掉 if (-- numAvail == 0) { removeFromPool(); } return toHandle(bitmapIdx); }
再看一下获取可用element过程,实际上最后调用到的是
private int findNextAvail0(int i, long bits) { final int maxNumElems = this.maxNumElems; //这里用来记录bitmap下标 final int baseVal = i << 6; for (int j = 0; j < 64; j ++) { if ((bits & 1) == 0) { // 记录他是page中第n个element int val = baseVal | j; if (val < maxNumElems) { return val; } else { break; } } bits >>>= 1; } return -1; }
这里将element所处bitmap的位置信息存储在一个int中,所以allocate中出现了再通过反向的位运算取出对应信息。最后执行了一个toHandle方法,这里
return 0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx;
实际上是把memoryMapIdx的信息加上了,这里的memoryMapIdx是所在chunk的信息,最终打包成一个long,那么通过这个long,就可以找到对应的chunk,subpage以及element的位置信息。对于PoolSubpage的free过程,基本上是allocate的逆过程,这里就不再说了。
下来看PoolChunk,我们照例先来看他的重要变量,
private final byte[] memoryMap; //subpage使用情况的树状结构 private final byte[] depthMap; //树的深度 private final PoolSubpage<T>[] subpages; // subpages
private final int maxOrder; //最大不超过21,决定subpage的数量 private final int chunkSize; // 这个chunk的大小
看一下构造函数中
PoolChunk(PoolArena<T> arena, T memory, int pageSize, int maxOrder, int pageShifts, int chunkSize, int offset) { ... assert maxOrder < 30 : "maxOrder should be < 30, but is: " + maxOrder; // maxSubpageAllocs 为 1<<10 即 2048 maxSubpageAllocs = 1 << maxOrder; // map的大小为 4096 memoryMap = new byte[maxSubpageAllocs << 1]; // 树的深度 depthMap = new byte[memoryMap.length]; int memoryMapIndex = 1; for (int d = 0; d <= maxOrder; ++ d) { int depth = 1 << d; for (int p = 0; p < depth; ++ p) { // 每个节点的值等于其所在层数 从0开始 memoryMap[memoryMapIndex] = (byte) d; depthMap[memoryMapIndex] = (byte) d; memoryMapIndex ++; } } // 2048个PoolSubpage subpages = newSubpageArray(maxSubpageAllocs); cachedNioBuffers = new ArrayDeque<ByteBuffer>(8); }
这里的核心就是,chunk中维护了一颗节点数目是subpage二倍的树来表示subpage的状态。树结构如下
0
1 1
2 2 2 2
叶子节点的数量等同于subpage的数量,数字越小,表示其拥有的叶子节点(即可分配的subpage)越多 = 2的(maxOrder - val)个。比如0表示它拥有4的叶子节点。
我们来看一下这个树的工作过程,即allocate的过程:首先会判断
normCapacity & subpageOverflowMask) != 0
要allocate的是否大于pageSize,我们就来看大于pageSize的情况吧
private long allocateRun(int normCapacity) { int d = maxOrder - (log2(normCapacity) - pageShifts); // 这里首先计算出来需要哪一个深度的节点 int id = allocateNode(d); }
这里的d实际上表示的是他需要多少个subpage,比如d =0 ,则它需要4个subpage
下来是分配节点的过程
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; } // 这里 id < initial (id & initial) == 0 时都会成立,也就是说,可以允许tree搜索到 deep = d 那一层,如果还没有符合条件的 // 那么下一层不可能有了 while (val < d || (id & initial) == 0) { id <<= 1; val = value(id); // 这里val > d 说明当前节点拥有的subpage少于需要的subpage // 那么去它的兄弟节点寻找 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); //将该节点设置为unusable setValue(id, unusable); // mark as unusable updateParentsAlloc(id); return id; }
随后逐级更新它的父节点
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; } }
我们再用刚才的tree来演示一下整个过程,malloc一个subpage,那么d = 2
0 0 1
1 1 --> 1 1 --> 2 1
2 2 2 2 3 2 2 2 3 2 2 2
最后init PooledByteBuf,实际上便是把对应的handle也就是chunk和subpage的信息返回给Bytebuf。
接下来的poolChunkList相对来说就简单了,他们分组的依据是根据chunk的使用率,当chunk的使用率发生变化,它会根据是否超越边界而把chunk移动到符合条件的poolChunkList中去。
我们最后来看一下PoolArena,惯例,先来看一下它的成员变量,
private final PoolSubpage<T>[] tinySubpagePools; // 前文提到的,存放小于8k的对象的池子 <512byte private final PoolSubpage<T>[] smallSubpagePools; 512byte< < 8k //根据使用率不同分组的chunk 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;
tinySubpagePools和smallSubpagePools会在构造函数中被初始化,其中tinySubpagePools包含32个subpage,每个subpage会作为一个header,allocate的subpage会以链表的形式跟在数组对应header后面。如,elemSize会加在tinySubpagePools[0]的next上,而header中是不保存内容的(存疑,凭什么,难道它牛逼吗?是不是因为他们是在PoolArena中,而真正存放内容的subpage要在chunk中,PoolArena中的subpage难以管理)。smallSubpagePools则是存放elemSize为512byte到8k之间的subpage。
我们接着来看它的核心函数 allocate
private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) { // 512byte一下为 16 byte的倍数 512byte以上为16*2的n次方 final int normCapacity = normalizeCapacity(reqCapacity); if (isTinyOrSmall(normCapacity)) { //小于8k的情况下 int tableIdx; PoolSubpage<T>[] table; boolean tiny = isTiny(normCapacity); if (tiny) { // < 512 if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) { return; } tableIdx = tinyIdx(normCapacity); table = tinySubpagePools; } else { if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) { return; } tableIdx = smallIdx(normCapacity); table = smallSubpagePools; } //首先先尝试从链表中取 final PoolSubpage<T> head = table[tableIdx]; synchronized (head) { final PoolSubpage<T> s = head.next; if (s != head) { assert s.doNotDestroy && s.elemSize == normCapacity; long handle = s.allocate(); assert handle >= 0; s.chunk.initBufWithSubpage(buf, null, handle, reqCapacity); incTinySmallAllocation(tiny); return; } } //如果链表中没有可用的poolSubpage,再通过chunk 来allocate synchronized (this) { allocateNormal(buf, reqCapacity, normCapacity); } incTinySmallAllocation(tiny); return; } // 大于 8k小于16M if (normCapacity <= chunkSize) { //先通过线程缓存allocate if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) { return; } synchronized (this) { allocateNormal(buf, reqCapacity, normCapacity); ++allocationsNormal; } } else { allocateHuge(buf, reqCapacity); } }
在最后,我们可以看到PoolArena有不同的派生类DirectArena和HeapArena,HeapArean就简单了,直接基于byte[]实现。而DirectArena就要复杂的多,根据cleaner,以及系统的不同,会有不同的实现。