一、简介

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使用内存池demo netty内存池设计_父节点

从图中可以看到,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内存的图形过程(图上的节点的值为层数)

  1. 从根节点a开始

netty使用内存池demo netty内存池设计_netty使用内存池demo_02

根节点的值为0,与层数2进行对比,0小于2,那么说明根节点以下还有足够的空间可以进行分配

  1. 根节点的左子节点b

netty使用内存池demo netty内存池设计_netty使用内存池demo_03

节点的值为1,与层数2进行对比,1小于2,那么说明此节点以下还有足够的空间可以进行分配

  1. 继续找节点b的左子节点

netty使用内存池demo netty内存池设计_父节点_04

节点的值为2,与层数2进行对比,2等于2,恩,就是它了

  1. 将节点c的值设置为12

netty使用内存池demo netty内存池设计_父节点_05

把它的值设置为12

  1. 比较节点c和节点d的值,取最小值

netty使用内存池demo netty内存池设计_父节点_06

  1. 比较节点b与节点e的值,取最小值

netty使用内存池demo netty内存池设计_子节点_07


基于以上分配3M内存过程,我们继续分配一个4M的内存

  1. 从根节点a开始比较

netty使用内存池demo netty内存池设计_子节点_08

节点a值为1,比要分配的4M的层数2小,所以继续与a的左子节点b对比

  1. 与节点b的值进行层数比较

netty使用内存池demo netty内存池设计_netty使用内存池demo_09

节点b的值为2等于4M所在层数2,但是节点b在memoryMap数组中的下标小于第二层第一个节点c在memorMap数组中的下标,说明肯定还有和父节点b的值一样的子节点(在分配了一
个节点后,父节点的值由两个子节点的最小值决定,所以当父节点的值不等于自身所在层数时,其值来源于子节点)

  1. 与节点c的值进行比较

netty使用内存池demo netty内存池设计_netty使用内存池demo_10

节点c的值为12,比4M所在层数2要大,说明没有足够的内存可以分配了

  1. 与节点d的值进行比较

netty使用内存池demo netty内存池设计_数组_11

节点d的值为2,与4M所在层数2相等并且节点d在memoryMap数组的下标是大于第二层第一个节点c在memorMap数组中的下标的

  1. 将节点d的值标记为12

netty使用内存池demo netty内存池设计_子节点_12

  1. 更新父节点b的值

netty使用内存池demo netty内存池设计_父节点_13

比较左子节点c与右子节点d的值,取最小值更新父节点b的值

  1. 继续往上更新a节点的值

netty使用内存池demo netty内存池设计_netty使用内存池demo_14

比较左子节点b与右子节点e的值,取最小值更新父节点a的值

看了内存的分配,现在我们来看看内存的释放

很显然,内存的释放就是内存分配的逆过程,假设我先释放节点c

  1. 将节点c的值恢复为层数2

netty使用内存池demo netty内存池设计_父节点_15

将节点c的值恢复为c所在的层数

  1. 比较节点c与节点d的值

    a. 用节点c与节点d的值去和当前节点所在层数(这里是2)进行比较,如果相等,那么父节点的值为c与d所在层数减去1

    b. 如果不相等,父节点的值取c与d的最小值
  2. netty使用内存池demo netty内存池设计_父节点_16


  3. 继续往上比较节点b与e的值

    a. 用节点b与节点e的值去和当前节点所在层数(这里是1)进行比较,如果相等,那么父节点的值为b与e所在层数减去1

    b. 如果不相等,父节点的值取c与d的最小值
  4. netty使用内存池demo netty内存池设计_父节点_17

  5. 释放节点d的内存

netty使用内存池demo netty内存池设计_子节点_18

将节点d的值恢复为节点d所在层数

  1. 比较节点c和节点d的值
    a. 用节点c与节点d的值去和当前节点所在层数(这里是2)进行比较,如果相等,那么父节点的值为c与d所在层数减去1
  2. netty使用内存池demo netty内存池设计_数组_19



  3. b. 如果不相等,父节点的值取c与d的最小值
  4. 继续往上比较节点b与e的值

    a. 用节点b与节点e的值去和当前节点所在层数(这里是1)进行比较,如果相等,那么父节点的值为b与e所在层数减去1
  5. netty使用内存池demo netty内存池设计_数组_20

  6. b. 如果不相等,父节点的值取c与d的最小值

3.2 大于等于512字节小于8k内存设计

netty将大于等于512字节小于8k的内存进行了划分,分别为512,1024,2048,4096,相邻两个元素之间相差2倍,映射到数组上为

netty使用内存池demo netty内存池设计_子节点_21

从图中可以看到每个PoolSubPage表示8k,第一个元素是用于分配512字节内存的,那么每个PoolSubPage内部需要划分成 8192 / 512 = 16 份,bitMap只需要1一个Long就可以
表示这16份512的内存

如果此时用户需要600字节的内存,其分配过程如下:

  1. 将600修正为最接近600的2的n次幂的值–》1024
  2. 从二叉树中寻找一个8k的内存,然后封装成PoolSubPage,将8k按1024分成8份
  3. 遍历bitMap数组,寻找元素值取反之后不等于零的,然后将这个元素进行位移,找到一个还分配的bit位,然后将其值置为1,表示已被分配

释放时只要找到当初分配内存的bit位重新置为0即可。

3.3 小于512字节的内存设计

netty将小于512字节的内存以16相间进行划分

netty使用内存池demo netty内存池设计_父节点_22

内存的申请和释放和 大于等于512字节小于8k内存设计 是一样的。