一、简介
netty作为一款优秀的通信框架,不可避免的需要面对频繁的数据读入与写出,此时肯定会导致大量ByteBuf对象的创建,为了减少频繁申请内存带来的开销与gc,netty设计了内存池。
二、内存池设计的演化
假设让你设计一个内存池,你会怎么设计?也许你会创建一个字节数组,然后分配一定的大小,像下面这样
//分配16M的字节数组
byte[] memoryPool = new byte[1 << 24];
首先我们按1字节进行划分
使用一个 Long[] bitMap = new Long[1 << 18]; 来记录被分配的内存
一个Long有64位,每一个bit位表示1字节,那么需要1 << 18个Long来表示这16M内存
此时我们要分配一个35字节的内存,那么我们从偏移0开始,数35个bit位将他们置为1,下次释放的时候再将这些置为1的全部重置为0即可
看起来好像挺不错的,但是它有一个致命的缺点,就是分配效率非常差,如果我们需要找到bit为0的会怎么做呢?
首先我们遍历bitMap数组,然后判断每个Long取反后是否等于0,等于0表示全部已经分配出去了,不为0的表示还有未分配掉的内存,然后通过位移操作从这个long中找出连续为0的bit
位,如果不够分配用户指定的内存a大小,那么继续判断下一个Long是否有足够的连续的内存,如果下一个Long中间出现断层,有一个bit为1,那么无法分配连续的内存a,则需要
跳过,从下一个为bit位为0的地方开始继续找连续内存空间。
为了提升效率,那么我们将上面的内存块按8kb进行划分,那么bitMap数组的长度就变成了 32
Long[] bitMap = new Long[1 << 5];
此时如果用户想要分配35字节的内存,我们自动给它升级到8k,如果用户想要15k,那么我们自动给它升级到16k,这样就大大提升了内存的分配效率,那还能不能再提升?
当然是有的,我们可以使用一个平衡二叉树提升其内存的分配效率(平衡二叉树的查询时间复杂度为log2(N)),从头节点16M开始往下平分,直到叶子节点为8字节,总共12层。
但是问题又来,用户只要35字节的内存,你却给他分配了8k的内存,造成(8192-35=8157)字节的空间浪费,太多了,无法接受
此时就出现了两极分化,一边是分配效率的问题,另一边是空间浪费的问题,增大页的大小,意味着空间的浪费,减小页的大小,意味着分配效率变差,那么怎么办?
netty想到一个招数,那就是分情况进行分配,如果分配的内存大于8k,那么页大小为8kb划分16M内存,使用二叉树查找,对于小于8kb的,又分为两种类型,一种是
tiny,用于分配512字节以下的,small用于分配512字节到8k之间的内存。下面一小节将对这三种类型的内存分配原理进行讲解。
三、netty内存池设计原理
3.1 大于8k内存设计
从图中可以看到,netty默认将一个PoolChunk设计成16M,每一个节点和兄弟节点平分父节点的内存,直到叶子节点大小为8k,总共12层,从0开始的话,那么最后一层为第11层。
从排序上来看,这是一个大顶堆,netty将这个二叉树存到一个 byte[] memoryMap 数组中,数组大小为2的12次方,从1开始,元素为二叉树的层数。类似如下:
byte[] memoryMap = {0,0,1,1,2,2,2,2…}
如果此时我们要分配一个3M的内存该怎么分配呢?
- 将3M转换成最接近3M的2的n次幂的值–》4M
- 计算4M所在层数–》maxOrder - (log2(4M) - log2(8k)) = 11 - (22 - 13) = 2
- 从根节点开始,从左子节点到右子节点进行遍历,比较每个节点上的值(存在memoryMap的层数)与2进行比较
- 左子节点的层数大于2,那么说明这个节点以下没有足够的内存分配,那么找到右节点R进行比较
- 右节点R的层数小于2,那么说明这个节点以下有足够的内存进行分配,那么继续比较右节点R的左子节点的层数,以此类推,直到找到等于2的那个节点并且这个节点所在数组memoryMap的下标不能小于4M所在层数第一个节点在数组memoryMap中的下标
- 找到层数为2的节点C后,将这个节点C的值修改为12,表示这个节点C已经被分配(以后分配的其他任何内存的层数都会小于等于11,所以这里标记成大于11的值都可以)
- 由于C被分配,那么这个C的父节点还剩下右节点可以分配,那么父节点就需要将值修改为右子节点的值,然后以此类推,直到根节点
下面是分配4M内存的图形过程(图上的节点的值为层数)
- 从根节点a开始
根节点的值为0,与层数2进行对比,0小于2,那么说明根节点以下还有足够的空间可以进行分配
- 根节点的左子节点b
节点的值为1,与层数2进行对比,1小于2,那么说明此节点以下还有足够的空间可以进行分配
- 继续找节点b的左子节点
节点的值为2,与层数2进行对比,2等于2,恩,就是它了
- 将节点c的值设置为12
把它的值设置为12
- 比较节点c和节点d的值,取最小值
- 比较节点b与节点e的值,取最小值
基于以上分配3M内存过程,我们继续分配一个4M的内存
- 从根节点a开始比较
节点a值为1,比要分配的4M的层数2小,所以继续与a的左子节点b对比
- 与节点b的值进行层数比较
节点b的值为2等于4M所在层数2,但是节点b在memoryMap数组中的下标小于第二层第一个节点c在memorMap数组中的下标,说明肯定还有和父节点b的值一样的子节点(在分配了一
个节点后,父节点的值由两个子节点的最小值决定,所以当父节点的值不等于自身所在层数时,其值来源于子节点)
- 与节点c的值进行比较
节点c的值为12,比4M所在层数2要大,说明没有足够的内存可以分配了
- 与节点d的值进行比较
节点d的值为2,与4M所在层数2相等并且节点d在memoryMap数组的下标是大于第二层第一个节点c在memorMap数组中的下标的
- 将节点d的值标记为12
- 更新父节点b的值
比较左子节点c与右子节点d的值,取最小值更新父节点b的值
- 继续往上更新a节点的值
比较左子节点b与右子节点e的值,取最小值更新父节点a的值
看了内存的分配,现在我们来看看内存的释放
很显然,内存的释放就是内存分配的逆过程,假设我先释放节点c
- 将节点c的值恢复为层数2
将节点c的值恢复为c所在的层数
-
比较节点c与节点d的值
a. 用节点c与节点d的值去和当前节点所在层数(这里是2)进行比较,如果相等,那么父节点的值为c与d所在层数减去1
b. 如果不相等,父节点的值取c与d的最小值
-
继续往上比较节点b与e的值
a. 用节点b与节点e的值去和当前节点所在层数(这里是1)进行比较,如果相等,那么父节点的值为b与e所在层数减去1
b. 如果不相等,父节点的值取c与d的最小值
-
释放节点d的内存
将节点d的值恢复为节点d所在层数
-
比较节点c和节点d的值
a. 用节点c与节点d的值去和当前节点所在层数(这里是2)进行比较,如果相等,那么父节点的值为c与d所在层数减去1
b. 如果不相等,父节点的值取c与d的最小值 -
继续往上比较节点b与e的值
a. 用节点b与节点e的值去和当前节点所在层数(这里是1)进行比较,如果相等,那么父节点的值为b与e所在层数减去1
b. 如果不相等,父节点的值取c与d的最小值
3.2 大于等于512字节小于8k内存设计
netty将大于等于512字节小于8k的内存进行了划分,分别为512,1024,2048,4096,相邻两个元素之间相差2倍,映射到数组上为
从图中可以看到每个PoolSubPage表示8k,第一个元素是用于分配512字节内存的,那么每个PoolSubPage内部需要划分成 8192 / 512 = 16 份,bitMap只需要1一个Long就可以
表示这16份512的内存
如果此时用户需要600字节的内存,其分配过程如下:
- 将600修正为最接近600的2的n次幂的值–》1024
- 从二叉树中寻找一个8k的内存,然后封装成PoolSubPage,将8k按1024分成8份
- 遍历bitMap数组,寻找元素值取反之后不等于零的,然后将这个元素进行位移,找到一个还分配的bit位,然后将其值置为1,表示已被分配
释放时只要找到当初分配内存的bit位重新置为0即可。
3.3 小于512字节的内存设计
netty将小于512字节的内存以16相间进行划分
内存的申请和释放和 大于等于512字节小于8k内存设计 是一样的。