在Netty内存池中,内存大小在8KB~16M的内存是由PoolChunk维护的,小于8KB的内存则是由PoolSubpage来维护的。而对于低于8KB的内存,Netty也是将其分红了两种状况0~496byte和512byte~8KB。其中,0~496byte的内存是由一个名称为tinySubpagePools的PoolSubpage的数组维护的,512byte~8KB的内存则是由名称为smallSubpagePools的PoolSubpage数组来维护的。本文首先会对tinySubpagePools和smallSubpagePools的总体结构进行说明,而后会讲解Netty是如何仅仅经过抽象出一种PoolSubpage的数据结构来实现对两种不一样的内存区间的管理的,最后本文会从PoolSubpage的源码的角度来说解PoolSubpage的实现原理。java
这里咱们直接查看这两个PoolSubpage数组的结构:算法
对于PoolSubpage的实现原理,其内部本质上是使用一个位图索引来表征某个内存块是否已经被占用了的。前面咱们讲到,每一个PoolSubpage的总内存大小都是8192byte,这里咱们以tinySubpagePools的第1号位的大小为16字节的PoolSubpage为例进行讲解(其实从这里就能够看出,前面咱们图中数组前面的数字就是表示当前节点链表中PoolSubpage所划分的内存块的大小)。数组
因为每一个内存块大小为16字节,而总大小为8192字节,于是总会有8192 / 16 = 512个内存块。为了对这些内存块进行标记,那么就须要一个长度为512的二进制位图索引进行表征。Netty并无使用jdk提供的BitMap这个类,而是使用了一个long型的数组。因为一个long占用的字节数为64,于是总共须要512 / 64 = 8个long型数字来表示。这也就是PoolSubpage中的long[] bitmap属性的做用。下图表示了PoolSubpage使用位图索引表示每一个内存块是否被使用的一个示意图:数据结构
这里须要说明的是,咱们这里是以每一个内存块的大小为16为例进行讲解的,而16是PoolSubpage所能维护的最小内存块,对于其余大小的内存块,其个数是比512要小的,可是PoolSubpage始终会声明一个长度为8的long型数组,而且声明一个bitmapLength来记录当前PoolSubpage中有几个long是用于标志内存块使用状况的。函数
对于PoolSubpage的实现原理,咱们这里首先对其各个属性进行讲解:优化
// 记录当前PoolSubpage的8KB内存块是从哪个PoolChunk中申请到的 final PoolChunk<T> chunk; // 当前PoolSubpage申请的8KB内存在PoolChunk中memoryMap中的下标索引 private final int memoryMapIdx; // 当前PoolSubpage占用的8KB内存在PoolChunk中相对于叶节点的起始点的偏移量 private final int runOffset; // 当前PoolSubpage的页大小,默认为8KB private final int pageSize; // 存储当前PoolSubpage中各个内存块的使用状况 private final long[] bitmap; PoolSubpage<T> prev; // 指向前置节点的指针 PoolSubpage<T> next; // 指向后置节点的指针 boolean doNotDestroy; // 表征当前PoolSubpage是否已经被销毁了 int elemSize; // 表征每一个内存块的大小,好比咱们这里的就是16 private int maxNumElems; // 记录内存块的总个数 private int bitmapLength; // 记录总共可以使用的bitmap数组的元素的个数 // 记录下一个可用的节点,初始为0,只要在该PoolSubpage中申请过一次内存,就会更新为-1, // 而后一直不会发生变化 private int nextAvail; // 剩余可用的内存块的个数 private int numAvail;
对于各个属性的初始化,咱们能够经过构造函数进行讲解,以下是其构造函数源码:this
PoolSubpage(PoolSubpage<T> head, PoolChunk<T> chunk, int memoryMapIdx, int runOffset, int pageSize, int elemSize) { this.chunk = chunk; this.memoryMapIdx = memoryMapIdx; this.runOffset = runOffset; this.pageSize = pageSize; // 初始化当前PoolSubpage总内存大小,默认为8KB // 计算bitmap长度,这里pageSize >>> 10其实就是将pageSize / 1024,获得的是8, // 从这里就能够看出,不管内存块的大小是多少,这里的bitmap长度永远是8,由于pageSize始终是不变的 bitmap = new long[pageSize >>> 10]; // 对其他的属性进行初始化 init(head, elemSize); } void init(PoolSubpage<T> head, int elemSize) { doNotDestroy = true; // elemSize记录了当前内存块的大小 this.elemSize = elemSize; if (elemSize != 0) { // 初始时,numAvail记录了可以使用的内存块个数,其个数能够经过pageSize / elemSize计算, // 咱们这里就是8192 / 16 = 512。maxNumElems指的是最大可以使用的内存块个数, // 初始时其是与可用内存块个数一致的。 maxNumElems = numAvail = pageSize / elemSize; nextAvail = 0; // 初始时,nextAvail是0 // 这里bitmapLength记录了可使用的bitmap的元素个数,这是由于,咱们示例使用的内存块大小是16, // 于是其总共有512个内存块,须要8个long才能记录,可是对于一些大小更大的内存块,好比smallSubpagePools // 中内存块为1024字节大小,那么其只有8192 / 1024 = 8个内存块,也就只须要一个long就能够表示, // 此时bitmapLength就是8。 // 这里的计算方式应该是bitmapLength = maxNumElems / 64,由于64是一个long的总字节数, // 可是Netty将其进行了优化,也就是这里的maxNumElems >>> 6,这是由于2的6次方正好为64 bitmapLength = maxNumElems >>> 6; // 这里(maxNumElems & 63) != 0就是判断元素个数是否小于64,若是小于,则须要将bitmapLegth加一。 // 这是由于若是其小于64,前面一步的位移操做结果为0,但其仍是须要一个long来记录 if ((maxNumElems & 63) != 0) { bitmapLength++; } // 对bitmap数组的值进行初始化 for (int i = 0; i < bitmapLength; i++) { bitmap[i] = 0; } } // 将当前PoolSubpage添加到PoolSubpage的链表中,也就是最开始图中的链表 addToPool(head); }
这里对于PoolSubpage的初始化主要是对bitmap、numAvail、bitmapLength的初始化,下面咱们看看其是如何经过这些属性来从PoolSubpage中申请内存的:3d
// 对于allocate()方法,其没有传入任何参数是由于当前PoolSubpage所能申请的内存块大小在构造方法中 // 已经经过elemSize指定了。 // 当前方法返回的是一个long型整数,这里是将要申请的内存块使用了一个long型变量进行表征了。因为一个内存块 // 是否使用是经过一个long型整数表示的,于是,若是想要表征当前申请到的内存块是这个long型整数中的哪一位, // 只须要一个最大为63的整数便可(long最多为64位),这只须要long型数的低6位就能够表示,因为咱们使用的是一个 // long型数组,于是还须要记录当前是在数组中第几个元素,因为数组长度最多为8,于是对于返回值的7~9位则是记录 // 了当前申请的内存块是在bitmap数组的第几个元素中。总结来讲,返回值的long型数的高32位中的低6位 // 记录了当前申请的是是bitmap中某个long的第几个位置的内存块,而高32位的7~9位则记录了申请的是bitmap数组 // 中的第几号元素。 // 这里说返回值的高32位是由于其低32位记录了当前8KB内存块是在PoolChunk中具体的位置,关于这一块的算法 // 读者能够阅读本人前面对PoolChunk进行讲解的文章 long allocate() { // 若是elemSize为0,则直接返回0 if (elemSize == 0) { return toHandle(0); } // 若是当前PoolSubpage没有可用的元素,或者已经被销毁了,则返回-1 if (numAvail == 0 || !doNotDestroy) { return -1; } // 计算下一个可用的内存块的位置 final int bitmapIdx = getNextAvail(); int q = bitmapIdx >>> 6; // 获取该内存块是bitmap数组中的第几号元素 int r = bitmapIdx & 63; // 获取该内存块是bitmap数组中q号位元素的第多少位 bitmap[q] |= 1L << r; // 将bitmap数组中q号元素的目标内存块位置标记为1,表示已经使用 // 若是当前PoolSubpage中可用的内存块为0,则将其从链表中移除 if (--numAvail == 0) { removeFromPool(); } // 将获得的bitmapIdx放到返回值的高32位中 return toHandle(bitmapIdx); }
这里allocate()方法首先会计算下一个可用的内存块的位置,而后将该位置标记为1,最后将获得的位置数据放到返回值的高32位中。这里咱们继续看其是如何计算下一个可用的位置的,以下是getNextAvail()的源码:指针
private int getNextAvail() { int nextAvail = this.nextAvail; // 若是是第一次尝试获取数据,则直接返回bitmap第0号位置的long的第0号元素, // 这里nextAvail初始时为0,在第一次申请以后就会变为-1,后面将再也不发生变化, // 经过该变量能够判断是不是第一次尝试申请内存 if (nextAvail >= 0) { this.nextAvail = -1; return nextAvail; } // 若是不是第一次申请内存,则在bitmap中进行遍历获取 return findNextAvail(); } private int findNextAvail() { final long[] bitmap = this.bitmap; final int bitmapLength = this.bitmapLength; // 这里的基本思路就是对bitmap数组进行遍历,首先判断其是否有未使用的内存是否所有被使用过 // 若是有未被使用的内存,那么就在该元素中找可用的内存块的位置 for (int i = 0; i < bitmapLength; i++) { long bits = bitmap[i]; if (~bits != 0) { // 判断当前long型元素中是否有可用内存块 return findNextAvail0(i, bits); } } return -1; } // 入参中i表示当前是bitmap数组中的第几个元素,bits表示该元素的值 private int findNextAvail0(int i, long bits) { final int maxNumElems = this.maxNumElems; final int baseVal = i << 6; // 这里baseVal就是将当前是第几号元素放到返回值的第7~9号位置上 // 对bits的0~63号位置进行遍历,判断其是否为0,为0表示该位置是可用内存块,从而将位置数据 // 和baseVal进行或操做,从而获得一个表征目标内存块位置的整型数据 for (int j = 0; j < 64; j++) { if ((bits & 1) == 0) { // 判断当前位置是否为0,若是为0,则表示是目标内存块 int val = baseVal | j; // 将内存快的位置数据和其位置j进行或操做,从而获得返回值 if (val < maxNumElems) { return val; } else { break; } } bits >>>= 1; // 将bits不断的向右移位,以找到第一个为0的位置 } return -1; }
上面的查找过程很是的简单,其原理起始就是对bitmap数组进行遍历,首先判断当前元素是否有可用的内存块,若是有,则在该long型元素中进行遍历,找到第一个可用的内存块,最后将表征该内存块位置的整型数据返回。这里须要说明的是,上面判断bitmap中某个元素是否有可用内存块是使用的是~bits != 0
来计算的,该算法的原理起始就是,若是一个long中全部的内存块都被申请了,那么这个long必然全部的位都为1,从总体上,这个long型数据的值就为-1,而将其取反~bits
以后,值确定就变为了0,于是这里只须要判断其取反以后是否等于0便可判断当前long型元素中是否有可用的内存块。code
下面咱们继续看PoolSubpage是如何对内存进行释放的,以下是free()方法的源码:
boolean free(PoolSubpage<T> head, int bitmapIdx) { if (elemSize == 0) { return true; } // 获取当前须要释放的内存块是在bitmap中的第几号元素 int q = bitmapIdx >>> 6; // 获取当前释放的内存块是在q号元素的long型数的第几位 int r = bitmapIdx & 63; // 将目标位置标记为0,表示可以使用状态 bitmap[q] ^= 1L << r; // 设置下一个可以使用的数据 setNextAvail(bitmapIdx); // numAvail若是等于0,表示以前已经被移除链表了,于是这里释放后须要将其添加到链表中 if (numAvail++ == 0) { addToPool(head); return true; } // 若是可用的数量小于最大数量,则表示其仍是在链表中,于是直接返回true if (numAvail != maxNumElems) { return true; } else { // else分支表示当前PoolSubpage中没有任何一个内存块被占用了 // 这里若是当前PoolSubpage的前置节点和后置节点相等,这表示其都是默认的head节点,也就是 // 说当前链表中只有一个可用于内存申请的节点,也就是当前PoolSubpage,这里就不会将其移除 if (prev == next) { return true; } // 若是有多个节点,则将当前PoolSubpage移除 doNotDestroy = false; removeFromPool(); return false; } }
能够看到,对于free()操做,主要是将目标位置标记为0,而后设置相关属性,而且判断是否须要将当前PoolSubpage添加到链表中或者从链表移除。
本文首先讲解了PoolSubpage的实现原理,而后讲解了其是如何控制内存的申请和释放的,最后从源码层面对其申请和释放内存的行为进行了讲解。