一、简介

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内存的图形过程(图上的节点的值为层数)

  1. 从根节点a开始

在这里插入图片描述

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

  1. 根节点的左子节点b

在这里插入图片描述

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

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

在这里插入图片描述

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

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

在这里插入图片描述

把它的值设置为12

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

在这里插入图片描述

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

在这里插入图片描述
基于以上分配3M内存过程,我们继续分配一个4M的内存

  1. 从根节点a开始比较

在这里插入图片描述

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

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

在这里插入图片描述

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

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

在这里插入图片描述

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

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

在这里插入图片描述

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

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

在这里插入图片描述

  1. 更新父节点b的值

在这里插入图片描述

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

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

在这里插入图片描述

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

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

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

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

在这里插入图片描述

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

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

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

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

    在这里插入图片描述

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

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

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

    在这里插入图片描述

  3. 释放节点d的内存

在这里插入图片描述

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

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

    在这里插入图片描述


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

  2. 继续往上比较节点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字节的内存,其分配过程如下:

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

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

3.3 小于512字节的内存设计

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

在这里插入图片描述

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