You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

6.2 KiB

该文所涉及的netty源码版本为4.1.16。

在一开始需要明确的几个概念

在Netty的内存池的PoolChunk中先要明确以下几个概念。

  • page: page是chunk中所能申请到的最小内存单位。
  • chunk: 一个chunk是一组page的集合
  • 在PoolChunk中chunkSize的大小是2^maxOrderpageSize其中2^maxOrder是PoolChunk中的完全二叉树叶子结点的数量pageSize则是单个page的大小。 综合如上所述举一个数字上的例子默认情况下单个Page的大小为8192也就是8kbmaxOrder默认情况下是11因此在这个情况下PoolChunk中的二叉树的叶子节点数量是2048chunkSize的大小则是20488kb为16M。

PoolChunk的内部完全二叉树结构

PoolChunk中的page通过一颗完全二叉树来达到快速访达及操作而不需要通过O(n)的时间复杂度来进行遍历并耗费相当大的空间来记录各个page的使用情况。一颗完全二叉树的结构如下所示

  • 高度=0 1 个节点 (单个节点表示的大小为chunkSize)
  • 高度=1 2个节点 (单个节点表示的大小为chunkSize/2)
  • ..
  • ..
  • 高度=d 2^d个节点 (单个节点表示的大小为chunkSize/2^d)
  • ..
  • 高度=maxOrder 2^maxOrder个节点 (单个节点的大小为chunkSize/2^maxOrder也就是pageSize)

在这棵树的帮助下当我们要申请x大小的内存的时候 得到比x最接近的chunkSize/2^k的大小也就是说只要从左开始找到k层第一个没有被使用的节点即可开始将其子树的叶子结点的page进行分配。

PoolChunk的二叉树使用状态

单依靠上述的完全二叉树是无法达到内存池设计的目的的因为缺少了page的使用情况仍旧需要一个数据结构来辅助记录各个节点的使用情况。
PoolChunk中还给出了一个byte数组memoryMap大小为完全二叉树所有节点的个数在之前的例子中这个byte数组就为4096。在初始情况下这个数组每个位置上的初始指为该位置的节点在完全二叉树中的高度。因此这个数组memoryMap就有了以下几种状态。

    1. memoryMap[i] = i节点在完全二叉树中的深度代表当前节点下的子树都还没有被分配。
    1. memoryMap[i] > i节点在完全二叉树中的深度, 这个节点下的子树也就有节点被使用,但是仍有节点处于空闲状态。
    1. memoryMap[i] = maxOrder + 1这个节点下面的子树已经完全被使用。 这个Byte数组就相当于为这个完全二叉树准备了状态与索引存储可以高效的在二叉树中选择定位所需要指定大小的子树进行分配。

业务逻辑展开

    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;
        }
        while (val < d || (id & initial) == 0) { // id & initial == 1 << d for all ids at depth d, for < d it is 0
            id <<= 1;
            val = value(id);
            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);
        setValue(id, unusable); // mark as unusable
        updateParentsAlloc(id);
        return id;
    }

allocateNode(int d)方法用来在完全二叉树中以从左开始的顺序获取一颗高度为d的没有被使用过的子树。具体顺序如下

  • 首先从根节点1开始判断memoryMap[1]的值如果大于d则说明当前的二叉树已经不存在能够分配的节点了。如果小于d则可以继续往下分配。
  • 如果其左节点在memoryMap的值小于d则继续从左节点往下寻找。如果大于则从其右节点开始往下寻找。
  • 在下一层的节点中持续进行上述的判断,直到在书中找到符合高度条件的子树。
    private long allocateRun(int normCapacity) {
        int d = maxOrder - (log2(normCapacity) - pageShifts);
        int id = allocateNode(d);
        if (id < 0) {
            return id;
        }
        freeBytes -= runLength(id);
        return id;
    }

allocateRun()方法就是在上文的allocateNode()的前提下根据指定的大小的内存在二叉树上分配指定大小的子树。比如说在上述16M大小每个page8kb的chunk中寻求64k的内存的时候需要8个page叶子结点那么就是需要一个高度为4的完全二叉树那么也就是只要在PoolChunk中通过allocateNode()方法从完全二叉树的第7层开始从左往右找到一颗可以使用的子树即可。

    private long allocateSubpage(int normCapacity) {
        // Obtain the head of the PoolSubPage pool that is owned by the PoolArena and synchronize on it.
        // This is need as we may add it back and so alter the linked-list structure.
        PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity);
        synchronized (head) {
            int d = maxOrder; // subpages are only be allocated from pages i.e., leaves
            int id = allocateNode(d);
            if (id < 0) {
                return id;
            }

            final PoolSubpage<T>[] subpages = this.subpages;
            final int pageSize = this.pageSize;

            freeBytes -= pageSize;

            int subpageIdx = subpageIdx(id);
            PoolSubpage<T> subpage = subpages[subpageIdx];
            if (subpage == null) {
                subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity);
                subpages[subpageIdx] = subpage;
            } else {
                subpage.init(head, normCapacity);
            }
            return subpage.allocate();
        }
    }

当向PoolChunk申请的内存大小小于pageSize的时候将直接通过allocateSubpage()方法尝试直接在叶子结点也就是二叉树的最后一层选择一个空的还未使用的叶子结点在选择的叶子结点中构造一个PoolSubPage来返回而不需要耗费整整一个叶子结点导致内存占用浪费。