Netty源码_内存管理(jemalloc4)

内存管理(jemalloc3) 这篇文章中,我们介绍了在Netty 4.1.45 版本之前使用的内存分配 jemalloc3 算法。

我们也说了 jemalloc3 算法的一个重大缺陷,就是对于 SmallNormal 规格类型,每个规格内存块相差都是一倍,这就可以导致 50% 的内存浪费;例如我们申请 513B 大小,那么只能分配1KB 规格的内存块。

一. 划分内存规格

1.1 与 jemalloc3 对比

jemalloc3_内存规格.png
jemalloc4_内存规格.png

对比上面两图, jemalloc4 内存分配算法和 jemalloc3 内存分配算法的确有很大不同啊。
jemalloc4 算法将内存分为三种类型:

内存规格 描述
Small 小规格内存块,容量从16B28KB 一共39 个内存规格
Normal 正常规格内存块,容量从32KB16MB 一共37 个内存规格
Huge 巨大内存块,不会放在内存管理中,直接内存中申请

你会发现没有了 Tiny 类型,而且每个规格内存相隔多少,好像从图中也看不到,只知道一共分为 76 个内存规格。

1.2 每个规格的内存大小

index log2Group log2Delta nDelta isMultiPageSize isSubPage log2DeltaLookup size log2Size pageIdxAndPages size2idxTab
0 4 4 0 0 1 4 16 4(4) 0
1 4 4 1 0 1 4 32 5(5) 1
2 4 4 2 0 1 4 48 5(6) 2
3 4 4 3 0 1 4 64 6(6) 3
4 6 4 1 0 1 4 80 6(7) 4
5 6 4 2 0 1 4 96 6(7) 5
6 6 4 3 0 1 4 112 6(7) 6
7 6 4 4 0 1 4 128 7(7) 7
8 7 5 1 0 1 5 160 7(8) 8->10
9 7 5 2 0 1 5 192 7(8) 10->12
10 7 5 3 0 1 5 224 7(8) 12->14
11 7 5 4 0 1 5 256 8(8) 14->16
12 8 6 1 0 1 6 320 8(9) 16->20
13 8 6 2 0 1 6 384 8(9) 20->24
14 8 6 3 0 1 6 448 8(9) 24->28
15 8 6 4 0 1 6 512 9(9) 28->32
16 9 7 1 0 1 7 640 9(10) 32->40
17 9 7 2 0 1 7 768 9(10) 40->48
18 9 7 3 0 1 7 896 9(10) 48->56
19 9 7 4 0 1 7 1024 10(10) 56->64
20 10 8 1 0 1 8 1280 10(11) 64->80
21 10 8 2 0 1 8 1536 10(11) 80->96
22 10 8 3 0 1 8 1792 10(11) 96->112
23 10 8 4 0 1 8 2048 11(11) 112->128
24 11 9 1 0 1 9 2560 11(12) 128->160
25 11 9 2 0 1 9 3072 11(12) 160->192
26 11 9 3 0 1 9 3584 11(12) 192->224
27 11 9 4 0 1 9 4096 12(12) 224->256
28 12 10 1 0 1 0 5120 12(13)
29 12 10 2 0 1 0 6144 12(13)
30 12 10 3 0 1 0 7168 12(13)
31 12 10 4 1 1 0 8192 13(13) 0(1)
32 13 11 1 0 1 0 10240 13(14)
33 13 11 2 0 1 0 12288 13(14)
34 13 11 3 0 1 0 14336 13(14)
35 13 11 4 1 1 0 16384 14(14) 1(2)
36 14 12 1 0 1 0 20480 14(15)
37 14 12 2 1 1 0 24576 14(15) 2(3)
38 14 12 3 0 1 0 28672 14(15)
39 14 12 4 1 0 0 32768 15(15) 3(4)
40 15 13 1 1 0 0 40960 15(16) 4(5)
41 15 13 2 1 0 0 49152 15(16) 5(6)
42 15 13 3 1 0 0 57344 15(16) 6(7)
43 15 13 4 1 0 0 65536 16(16) 7(8->9)
44 16 14 1 1 0 0 81920 16(17) 8(10->11)
45 16 14 2 1 0 0 98304 16(17) 9(12->13)
46 16 14 3 1 0 0 114688 16(17) 10(14->15)
47 16 14 4 1 0 0 131072 17(17) 11(16->19)
48 17 15 1 1 0 0 163840 17(18) 12(20->23)
49 17 15 2 1 0 0 196608 17(18) 13(24->27)
50 17 15 3 1 0 0 229376 17(18) 14(28->31)
51 17 15 4 1 0 0 262144 18(18) 15(32->39)
52 18 16 1 1 0 0 327680 18(19) 16(40->47)
53 18 16 2 1 0 0 393216 18(19) 17(48->55)
54 18 16 3 1 0 0 458752 18(19) 18(56->63)
55 18 16 4 1 0 0 524288 19(19) 19(64->79)
56 19 17 1 1 0 0 655360 19(20) 20(80->95)
57 19 17 2 1 0 0 786432 19(20) 21(96->111)
58 19 17 3 1 0 0 917504 19(20) 22(112->127)
59 19 17 4 1 0 0 1048576 20(20) 23(128->159)
60 20 18 1 1 0 0 1310720 20(21) 24(160->191)
61 20 18 2 1 0 0 1572864 20(21) 25(192->223)
62 20 18 3 1 0 0 1835008 20(21) 26(224->255)
63 20 18 4 1 0 0 2097152 21(21) 27(256->319)
64 21 19 1 1 0 0 2621440 21(22) 28(320->383)
65 21 19 2 1 0 0 3145728 21(22) 29(384->447)
66 21 19 3 1 0 0 3670016 21(22) 30(448->511)
67 21 19 4 1 0 0 4194304 22(22) 31(512->639)
68 22 20 1 1 0 0 5242880 22(23) 32(640->767)
69 22 20 2 1 0 0 6291456 22(23) 33(768->895)
70 22 20 3 1 0 0 7340032 22(23) 34(896->1023)
71 22 20 4 1 0 0 8388608 23(23) 35(1024->1279)
72 23 21 1 1 0 0 10485760 23(24) 36(1280->1535)
73 23 21 2 1 0 0 12582912 23(24) 37(1536->1791)
74 23 21 3 1 0 0 14680064 23(24) 38(1792->2047)
75 23 21 4 1 0 0 16777216 24(24) 39(2048)

先解释每个表头的含义:

  • index: 内存规格的索引,一共有76 个内存规格,因此索引就是 0 -> 75
  • log2Group: 每一个内存规格组的基础大小的 log2 对数。
  • log2Delta: 每一个内存规格组组内内存增量的 log2 对数。
  • nDelta: 每一个内存规格组增量的个数。
  • isMultiPageSize: 表示是不是 pageSize 的倍数。
  • isSubPage: 表示是不是 isSubPage 类型,就是 Small 类型。
  • log2DeltaLookup: 对于方便较小size 快速查询对应规格大小的辅助值。
  • size: 表示这个内存规格对应的内存大小。
  • log2Size: 表示这个内存规格对应内存大小的 log2 对数,括号里的数是能容纳这个内存大小最近的log2 对数。

    例如 48, 它的 log2 对数是 5,但是能容纳 48 最近的log2 对数却是 6

  • pageIdxAndPages: 表示pages 对应的下标,括号里面是它的范围。

    因为内存规格增量会越来越大,后面会出现两个内存规格差值是 pageSize 的几倍,所以里面就表示它的范围。

  • size2idxTab: 是用来方便查找较小size 快速查询对应规格大小。

仔细观察表中数据,我们得出如下特点:

  • 每一组有4 种内存规格,组内每一种内存规格的差值是一样的,大小都是(1 << log2Delta)。
  • 除了第一组外,每一组的 log2Grouplog2Delta 都比上一组增加了 1
  • 除了第一组外,其他组的log2Group == log2Delta + 2,第一组 log2Group == log2Delta
  • 除了第一组外,其他组的nDelta 都是从1 开始的,第一组是从 0 开始的。
  • 除了第一组外,其他组第一个内存规格正好是上一个组第一个内存规格大小的两倍。

我们知道内存规格内存大小的计算公式

size = 1 << log2Group + nDelta * (1 << log2Delta)

除第一组外,每一组的 nDelta1 开始,log2Group == log2Delta + 2,并且每一组 log2Grouplog2Delta 都比上一组增加了 1;因此每一组第一个内存规格正好是上一个组第一个内存规格大小的两倍。

二. SizeClasses

这个类的作用就是处理 jemalloc4 算法的内存大小分配的。

2.1 重要属性

  • short[][] sizeClasses :是一个二维数组,一共是 76 个 长度为7short[] 一维数组组成。

    short[7] 就存储上面表格前 7 标头数据。

  • int[] sizeIdx2sizeTab:用来记录 index 对应的内存规格大小。

    数组长度是 76,存储了上面表格 size 的数据,数组下标就是 index

  • int[] pageIdx2sizeTab : 用来记录 pageIdxAndPages 对应的内存规格大小。

    数组长度是 40,存储了上面表格 size 的数据,数组下标就是 pageIdxAndPages

  • int[] size2idxTab : 用来较小size 对应 sizeIdx 的值,这样我们再查找较小值对应的内存规格时,可以直接通过这个数组得到。

    数组长度是 256,就是上面表格 size2idxTab 相关值。
    例如,我们求 66 对应的内存规格,(66 - 1)/16 = 3, 通过 size2idxTab 得到sizeIdx也是 3;如果是 257, (257 - 1)/16 = 16,通过 size2idxTab 得到sizeIdx也是 11

2.2 重要属方法

public interface SizeClassesMetric {

    /**
     * 根据给定的数组索引 [sizeIdx] 从 [sizeIdx2sizeTab] 数组中获取对应的内存大小
     *
     * @return size
     */
    int sizeIdx2size(int sizeIdx);

    /**
     * 根据给定的数组索引 [sizeIdx],计算出对应的内存大小
     *
     * @return size
     */
    int sizeIdx2sizeCompute(int sizeIdx);

    /**
     * 根据给定的数组索引 [pageIdx] 从 [pageIdx2sizeTab] 数组中获取对应的内存大小
     *
     * @return size which is multiples of pageSize.
     */
    long pageIdx2size(int pageIdx);

    /**
     * 根据给定的数组索引 [pageIdx],计算出对应的内存大小
     *
     * @return size which is multiples of pageSize
     */
    long pageIdx2sizeCompute(int pageIdx);

    /**
     *
     * 根据请求大小 [size] 得到规格化内存的索引值[sizeIdx]
     *
     */
    int size2SizeIdx(int size);

    /**
     *
     * 根据请求大小 [pages] 得到规格化内存的索引值[pageIdx]
     *
     * @return pageIdx of the pageSize class
     */
    int pages2pageIdx(int pages);

    /**
     * 根据请求大小 [pages] 得到规格化内存的索引值[pageIdx],
     * 与 pages2pageIdx(int pages) 方法不同,它会向下求得最近的[pageIdx]。
     *
     */
    int pages2pageIdxFloor(int pages);

    /**
     * 以指定的大小和对齐方式分配对象,使可用大小标准化。
     */
    int normalizeSize(int size);
}

2.2.1 sizeIdx2size 方法

    @Override
    public int sizeIdx2size(int sizeIdx) {
        return sizeIdx2sizeTab[sizeIdx];
    }

2.2.2 sizeIdx2sizeCompute 方法

    @Override
    public int sizeIdx2sizeCompute(int sizeIdx) {

        /**
         * LOG2_SIZE_CLASS_GROUP = 2: 说明每一组group的数量就是4个;
         * LOG2_QUANTUM = 4: 最小容量从16,即2的4次方开始。
         * 计算 size 的值,必须要区分第一组和后面的组,这个计算逻辑不同。
         */
        // 因为每一组数量就是4, >> LOG2_SIZE_CLASS_GROUP 就是得到组group;
        // 0 表示第一组,1就是第二组
        int group = sizeIdx >> LOG2_SIZE_CLASS_GROUP;
        // (1 << LOG2_SIZE_CLASS_GROUP) - 1 = 3;
        // 和 sizeIdx 进行位与(&) 运算就是得到整除4的余数
        int mod = sizeIdx & (1 << LOG2_SIZE_CLASS_GROUP) - 1;

        /**
         * 如果是第一组(group == 0),那么 groupSize就是0,最后size只通过 modSize 得到;
         * 其他组,那么组的基础size: 1 << ( 5 + group), LOG2_QUANTUM + LOG2_SIZE_CLASS_GROUP - 1 的值就是 5,
         * 这个计算公式就相当于 1 << log2Group; log2Group = 5 + group
         */
        int groupSize = group == 0? 0 :
                1 << LOG2_QUANTUM + LOG2_SIZE_CLASS_GROUP - 1 << group;

        // 第一组 shift == 1
        // 其他组 shift = group,其实第二组也是 shift == group == 1
        int shift = group == 0? 1 : group;
        // lgDelta 就是 log2Delta 的值,第一组和第二组都是4,第三组是5
        int lgDelta = shift + LOG2_QUANTUM - 1;
        /**
         * 除第一组外,其他组的 nDelta 都是从1开始的,所以 nDelta = mod + 1;
         * 第一组的 nDelta 从0开始的,但是这里也使用了 mod + 1,那是因为我们直接将 groupSize = 0
         */
        int modSize = mod + 1 << lgDelta;

        return groupSize + modSize;
    }

2.2.3 pageIdx2size 方法

    @Override
    public long pageIdx2size(int pageIdx) {
        return pageIdx2sizeTab[pageIdx];
    }

2.2.4 pageIdx2sizeCompute 方法

    @Override
    public long pageIdx2sizeCompute(int pageIdx) {
        /**
         * 这个的逻辑和 sizeIdx2sizeCompute() 几乎一模一样,
         * 唯一不同的是 sizeIdx2sizeCompute() 是从 LOG2_QUANTUM (4) 开始
         * 而这个方法是从 pageShifts (pageSize 13) 开始
         */
        int group = pageIdx >> LOG2_SIZE_CLASS_GROUP;
        int mod = pageIdx & (1 << LOG2_SIZE_CLASS_GROUP) - 1;

        long groupSize = group == 0? 0 :
                1L << pageShifts + LOG2_SIZE_CLASS_GROUP - 1 << group;

        int shift = group == 0? 1 : group;
        int log2Delta = shift + pageShifts - 1;
        int modSize = mod + 1 << log2Delta;

        return groupSize + modSize;
    }

2.2.5 size2SizeIdx 方法

    /**
     * 根据请求 size 得到对应的 sizeIdx,
     * 这个 sizeIdx 就是 sizeIdx2sizeTab 数组的下标。
     * 其实就是得到这个 size 对应的规格化内存块大小
     */
    @Override
    public int size2SizeIdx(int size) {
        if (size == 0) {
            return 0;
        }
        // 如果大于 chunkSize,说明它是一个 Huge 类型内存块;
        // 那么返回 nSizes,超出 sizeIdx2sizeTab 数组的范围。
        if (size > chunkSize) {
            return nSizes;
        }

        // 是否需要进行内存对齐,就是 size 必须是 directMemoryCacheAlignment 的倍数。
        // directMemoryCacheAlignment 必须是 2 的幂数,这样才能进行位运算。
        if (directMemoryCacheAlignment > 0) {
            size = alignSize(size);
        }

        // 如果 size 小于等于 lookupMaxSize,
        // 那么就在 size2idxTab 数组范围内,
        // 可以通过 size2idxTab 数组快速得到 sizeIdx 值。
        if (size <= lookupMaxSize) {
            // >> LOG2_QUANTUM 相当于 (size - 1) / 16,因为肯定都是 16 的倍数
            // 获取 sizeIdx 再通过 sizeIdx2sizeTab 数组得到对应大小
            return size2idxTab[size - 1 >> LOG2_QUANTUM];
        }


        /**
         * ((size << 1) - 1) 是向上得到 size 对应的 log2 的数;
         * 例如 8 得到 3,9 得到 4,15 得到 4, 16 得到 4, 17 得到 5.
         *
         *  LOG2_SIZE_CLASS_GROUP + LOG2_QUANTUM + 1 的值就是 7。
         *  而第一组规格内存块 size 最大就是 64, 计算后的值就是 6;
         *  如果我们需要大小为 65,计算后的值就是 7, 在第二组的第一个规格内存块。
         *
         *  因此我们可以得出,
         *  只要 x 小于 7,这个 size 就是在第一组内;
         *  x 大于等于 7,这个 size 在除第一组外的其他组内。
         *
         *  第一组和其他组的算法不一样
         */
        int x = log2((size << 1) - 1);
        // 判断是第一组还是其他组。
        // 第一组 shift 是 0,第二组是 1, 第三组是 2,依次递增1
        int shift = x < LOG2_SIZE_CLASS_GROUP + LOG2_QUANTUM + 1
                ? 0 : x - (LOG2_SIZE_CLASS_GROUP + LOG2_QUANTUM);

        // 因为每一组默认是 4 个,就是 shift << LOG2_SIZE_CLASS_GROUP,
        // 得到这一组的开始值。
        int group = shift << LOG2_SIZE_CLASS_GROUP;

        // 判断是第一组还是其他组。
        // 第一组就是 4, 第二组也是 4,第三组是5,依次递增1
        int log2Delta = x < LOG2_SIZE_CLASS_GROUP + LOG2_QUANTUM + 1
                ? LOG2_QUANTUM : x - LOG2_SIZE_CLASS_GROUP - 1;

        int deltaInverseMask = -1 << log2Delta;
        int mod = (size - 1 & deltaInverseMask) >> log2Delta &
                  (1 << LOG2_SIZE_CLASS_GROUP) - 1;
        

        return group + mod;
    }

2.2.6 pages2pageIdxpages2pageIdxFloor 方法

    @Override
    public int pages2pageIdx(int pages) {
        return pages2pageIdxCompute(pages, false);
    }

    @Override
    public int pages2pageIdxFloor(int pages) {
        return pages2pageIdxCompute(pages, true);
    }

    private int pages2pageIdxCompute(int pages, boolean floor) {
        int pageSize = pages << pageShifts;
        if (pageSize > chunkSize) {
            return nPSizes;
        }

        int x = log2((pageSize << 1) - 1);

        int shift = x < LOG2_SIZE_CLASS_GROUP + pageShifts
                ? 0 : x - (LOG2_SIZE_CLASS_GROUP + pageShifts);

        int group = shift << LOG2_SIZE_CLASS_GROUP;

        int log2Delta = x < LOG2_SIZE_CLASS_GROUP + pageShifts + 1?
                pageShifts : x - LOG2_SIZE_CLASS_GROUP - 1;

        int deltaInverseMask = -1 << log2Delta;
        int mod = (pageSize - 1 & deltaInverseMask) >> log2Delta &
                  (1 << LOG2_SIZE_CLASS_GROUP) - 1;

        int pageIdx = group + mod;

        // floor 是true, 当 pageIdx 对应的内存大小比 pages 大时,pageIdx要减一
        if (floor && pageIdx2sizeTab[pageIdx] > pages << pageShifts) {
            pageIdx--;
        }

        return pageIdx;
    }

三. PoolChunk 方法

先理解重要概念:

  • page: 它是PoolChunk 能分配内存块的最小单位。
  • run: 一个 run 中包含多个 page;当然也可以只包含一个 page,就是单pagerun
  • chunk: 就是表示这个PoolChunk,包含多个 run

PoolChunk 进行内存块的分配,就是分配出不同的 run,如下图:

 *     /-----------------\
 *     | run             |
 *     |                 |
 *     |                 |
 *     |-----------------|
 *     | run             |
 *     |                 |
 *     |-----------------|
 *     | unalloctated    |
 *     | (freed)         |
 *     |                 |
 *     |-----------------|
 *     | subpage         |
 *     |-----------------|
 *     | unallocated     |
 *     | (freed)         |
 *     | ...             |
 *     | ...             |
 *     | ...             |
 *     |                 |
 *     |                 |
 *     |                 |
 *     \-----------------/

3.1 重要属性

3.1.1 handle

那么怎么代表不同的 run 呢,就是用一个 long 类型的 handle 来表示:

oooooooo ooooooos ssssssss ssssssue bbbbbbbb bbbbbbbb bbbbbbbb bbbbbbbb

handlelong 类型,它的不同 bits 的意义如下:

  • o:前十五位bit 表示这个内存块run 的页偏移量 runOffset

    因为 PoolChunk 是按照页来进行分割的,页偏移量 runOffset表示这个内存块run 在这个 PoolChunkrunOffset 页的位置。

  • s:中间十五位bit 表示这个内存块run 拥有的页数size
  • u:一位bit 表示这个内存块run 是否被使用。
  • e:一位bit 表示这个内存块run 是否是 isSubpage
  • b: 一共32位,表示subpagebitmapIdx

3.1.2 runsAvail

    private final LongPriorityQueue[] runsAvail;
  • 它是一个优先级队列LongPriorityQueue 的数组,数组长度就是pageIdx 数组的大小,默认就是 40
  • 它管理者所有可分配的 run 内存块。run 根据拥有的页数不同可以分为 40 中,所以 runsAvail 中每一个优先级队列管理一种类型run 内存块。
  • LongPriorityQueue是一个优先级队列,利用堆排序来实现的,优先级就是run 内存块的页偏移量runOffset
  • 初始时,runsAvail 在下标 39 的优先级队列中存放这个整块PoolChunkrun 内存块,即 runOffset0, sizechunkSize/pageSize

3.1.3 runsAvailMap

    private final LongLongHashMap runsAvailMap;
  • 将它看成一个 Map,存放着所有可分配的 run 内存块第一个和最后一个页偏移量runOffset,以及run 内存块的handle
  • runsAvailMap 中的 key 就是页偏移量runOffsetvalue 就是内存块的handle
  • 它作用就是当释放内存块run 的时候,可以将相连可分配合并在一起。

3.1.4 subpages

private final PoolSubpage<T>[] subpages
  • 管理这个PoolChunk 下的所有 PoolSubpage
  • 特别注意 PoolSubpage 的大小不固定是 pageSize,有可能是几个 pageSize 的大小。

3.2 分配内存块

3.2.1 allocate 方法

    boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int sizeIdx, PoolThreadCache cache) {
        final long handle;
        if (sizeIdx <= arena.smallMaxSizeIdx) {
            // small
            handle = allocateSubpage(sizeIdx);
            if (handle < 0) {
                return false;
            }
            // 肯定是 isSubpage
            assert isSubpage(handle);
        } else {
            // normal
            // runSize must be multiple of pageSize
            // 是 Normal 类型,先根据 sizeIdx 获取对应的规格内存块大小,肯定是 pageSize 的倍数
            int runSize = arena.sizeIdx2size(sizeIdx);
            handle = allocateRun(runSize);
            if (handle < 0) {
                return false;
            }
        }

        ByteBuffer nioBuffer = cachedNioBuffers != null? cachedNioBuffers.pollLast() : null;
        initBuf(buf, nioBuffer, handle, reqCapacity, cache);
        return true;
    }
  • 如果是 Small 规格类型,那么就调用 allocateSubpage(sizeIdx) 方法,分配内存块。注意 Small 类型最大是 28KB,超过pageSize 的大小。
  • 如果是 Normal 规格类型,那么就调用 allocateRun(runSize) 方法,分配内存块。

3.2.2 allocateRun 方法

    private long allocateRun(int runSize) {
        int pages = runSize >> pageShifts;
        // 根据需要的 pages 得到对应的 pageIdx,
        // 也就是 run 的规格
        int pageIdx = arena.pages2pageIdx(pages);

        synchronized (runsAvail) {
            //find first queue which has at least one big enough run
            // 寻找第一个足够大能分配需求 run 规格的内存块
            int queueIdx = runFirstBestFit(pageIdx);
            if (queueIdx == -1) {
                // 表示当前 PoolChunk 已经没有足够的空间分配这个 runSize
                return -1;
            }

            //get run with min offset in this queue
            LongPriorityQueue queue = runsAvail[queueIdx];
            // 得到这个能分配的内存块,这里其实已经从 queue 中删除了这个内存块了
            long handle = queue.poll();

            // 这个内存块肯定是有效的
            assert handle != LongPriorityQueue.NO_VALUE && !isUsed(handle) : "invalid handle: " + handle;

            // 从队列中移除这个可用内存块
            removeAvailRun(queue, handle);

            if (handle != -1) {
                // 将这个可用内存块分割成两部分:
                // 一块是 pages 大小,返回给调用者使用。
                // 一块是剩余大小,表示还能分配内存块,还需要存入 runsAvail 中
                handle = splitLargeRun(handle, pages);
            }

            // runSize(pageShifts, handle) 表示返回内存块的大小
            // 当前 PoolChunk 的可用字节数要减少
            freeBytes -= runSize(pageShifts, handle);
            return handle;
        }
    }
  • runsAvail 中寻找足够大能分配需求可分配run内存块。
  • 调用 splitLargeRun(handle, pages) 将可用内存块分割成两部分,一块是 pages 大小,返回给调用者使用;一块是剩余大小,表示还能分配内存块,还需要存入 runsAvail 中。

3.2.3 allocateSubpage 方法

   private long allocateSubpage(int sizeIdx) {
        // 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(sizeIdx);
        synchronized (head) {
            //allocate a new run
            // 计算得到的值 runSize 必须是 pageSize 的倍数,
            int runSize = calculateRunSize(sizeIdx);
            //runSize must be multiples of pageSize
            // runSize 必须是 pageSize 的倍数,
            // 因为分配可用内存块 run 就是按照几个 pageSize 分配的
            long runHandle = allocateRun(runSize);
            if (runHandle < 0) {
                return -1;
            }

            int runOffset = runOffset(runHandle);
            assert subpages[runOffset] == null;
            int elemSize = arena.sizeIdx2size(sizeIdx);

            PoolSubpage<T> subpage = new PoolSubpage<T>(head, this, pageShifts, runOffset,
                               runSize(pageShifts, runHandle), elemSize);

            subpages[runOffset] = subpage;
            // 返回 handle 是isSubpage, 且包含 bitmapIdx
            return subpage.allocate();
        }
    }
  • 调用 calculateRunSize(sizeIdx) 方法,计算出即能整除需要的Small 类型内存大小,又能整除pageSize 的最小值。因为我们分配 run 内存块大小只能是 pageSize 的倍数。
  • 调用 allocateRun(runSize) 方法分配run 内存块。
  • 创建 PoolSubpage 对象,分配需求的Small 类型内存块。

3.2.3 initBuf 方法

    void initBuf(PooledByteBuf<T> buf, ByteBuffer nioBuffer, long handle, int reqCapacity,
                 PoolThreadCache threadCache) {
        if (isRun(handle)) {
            // 如果是 Normal 规格类型
            // 真实偏移量就是 handle 的 (runOffset * pageSize)
            // 大小就是 handle 的 (runSize * pageSize)
            buf.init(this, nioBuffer, handle, runOffset(handle) << pageShifts,
                     reqCapacity, runSize(pageShifts, handle), arena.parent.threadCache());
        } else {
            initBufWithSubpage(buf, nioBuffer, handle, reqCapacity, threadCache);
        }
    }

    void initBufWithSubpage(PooledByteBuf<T> buf, ByteBuffer nioBuffer, long handle, int reqCapacity,
                            PoolThreadCache threadCache) {
        int runOffset = runOffset(handle);
        int bitmapIdx = bitmapIdx(handle);

        PoolSubpage<T> s = subpages[runOffset];
        assert s.doNotDestroy;
        assert reqCapacity <= s.elemSize;

        // 真实偏移量就是handle 的 ((runOffset * pageSize) + (bitmapIdx * s.elemSize))
        // 大小就是 elemSize
        int offset = (runOffset << pageShifts) + bitmapIdx * s.elemSize;
        buf.init(this, nioBuffer, handle, offset, reqCapacity, s.elemSize, threadCache);
    }

初始化池缓冲区 PooledByteBuf, 只需要计算出偏移量和大小就可以了。

3.2.4 runFirstBestFit

    /**
     * 根据 pageIdx 寻找第一个足够大规格的 run
     */
    private int runFirstBestFit(int pageIdx) {
        if (freeBytes == chunkSize) {
            // 如果这个 PoolChunk 还没有进行分配,直接
            return arena.nPSizes - 1;
        }
        // 从刚满足run规格的 pageIdx 开始,一直遍历到最大run 规格(arena.nPSizes -1)
        // 从 PoolChunk 中寻找能分配的 run
        for (int i = pageIdx; i < arena.nPSizes; i++) {
            LongPriorityQueue queue = runsAvail[i];
            if (queue != null && !queue.isEmpty()) {
                return i;
            }
        }
        return -1;
    }

3.2.5 splitLargeRun

    private long splitLargeRun(long handle, int needPages) {
        assert needPages > 0;

        // 获取这个可用内存块 run 一共有多少个 pages
        int totalPages = runPages(handle);
        assert needPages <= totalPages;

        // 还剩余的内存块大小
        int remPages = totalPages - needPages;

        if (remPages > 0) {
            // 得到内存块 run 的偏移量
            int runOffset = runOffset(handle);

            // keep track of trailing unused pages for later use
            // runOffset + needPages 表示剩余可用内存块的偏移量
            int availOffset = runOffset + needPages;
            // 得到新的剩余可用的内存块 run 的handle
            long availRun = toRunHandle(availOffset, remPages, 0);
            // 将剩余可用的内存块 run 存到 runsAvail 中
            insertAvailRun(availOffset, remPages, availRun);

            // 返回给调用者的内存块 run 的handle
            return toRunHandle(runOffset, needPages, 1);
        }

        //mark it as used
        // 将这个 handle 变成已使用
        handle |= 1L << IS_USED_SHIFT;
        return handle;
    }

3.2.6 calculateRunSize

    /**
     * 计算得到 runSize 必须是 pageSize 的倍数
     */
    private int calculateRunSize(int sizeIdx) {
        int maxElements = 1 << pageShifts - SizeClasses.LOG2_QUANTUM; // 512
        int runSize = 0;
        int nElements;

        // 得到 sizeIdx 对应规格化内存块的大小
        final int elemSize = arena.sizeIdx2size(sizeIdx);

        //find lowest common multiple of pageSize and elemSize
        // 就是找到最小 pageSize 的倍数 runSize,能够整除 elemSize
        // runSize != (runSize / elemSize) * elemSize 就表示 runSize 能够整除 elemSize
        do {
            runSize += pageSize;
            nElements = runSize / elemSize;
        } while (nElements < maxElements && runSize != nElements * elemSize);

        while (nElements > maxElements) {
            runSize -= pageSize;
            nElements = runSize / elemSize;
        }

        assert nElements > 0;
        assert runSize <= chunkSize;
        assert runSize >= elemSize;

        return runSize;
    }

3.3 释放内存块

3.3.1 free

    void free(long handle, int normCapacity, ByteBuffer nioBuffer) {
        if (isSubpage(handle)) {
            // 如果是 small 类型
            int sizeIdx = arena.size2SizeIdx(normCapacity);
            // 找到 PoolArena 中这个small 规格类型对应 PoolSubpage 链表
            PoolSubpage<T> head = arena.findSubpagePoolHead(sizeIdx);

            // 从 handle 中得到这个内存块 run 的偏移量 runOffset
            int sIdx = runOffset(handle);
            // 通过这个偏移量 runOffset 得到对应的 PoolSubpage
            PoolSubpage<T> subpage = subpages[sIdx];
            assert subpage != null && subpage.doNotDestroy;

            // 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.
            synchronized (head) {
                if (subpage.free(head, bitmapIdx(handle))) {
                    //the subpage is still used, do not free it
                    // 这个 PoolSubpage 还在使用,不能释放这块内存块 run
                    return;
                }
                assert !subpage.doNotDestroy;
                // Null out slot in the array as it was freed and we should not use it anymore.
                // 这里和之前不一样,直接设置为null,的确不需要复用,没有多大意义
                subpages[sIdx] = null;
            }
        }

        // 程序运行到这里肯定需要是否这个 handle 对应的内存块 run
        int pages = runPages(handle);

        synchronized (runsAvail) {
            // collapse continuous runs, successfully collapsed runs
            // will be removed from runsAvail and runsAvailMap
            // 在这个要释放的内存块周围,不断尝试向前或者向后合并可用内存块,
            // 组成一个大的内存块
            long finalRun = collapseRuns(handle);

            //set run as not used
            // 设置成未使用
            finalRun &= ~(1L << IS_USED_SHIFT);
            //if it is a subpage, set it to run
            // 设置成非 subpage
            finalRun &= ~(1L << IS_SUBPAGE_SHIFT);

            // 将这个可用内存块run 插入到 runsAvail 中
            insertAvailRun(runOffset(finalRun), runPages(finalRun), finalRun);
            // 增加当前 PoolChunk 的可用字节数
            // 注意,这里一定不能用 runPages(finalRun),
            // 因为虽然我们可能合并了一个大的可用内存块,但是被合并的内存块原来就是可用的。
            freeBytes += pages << pageShifts;
        }

        if (nioBuffer != null && cachedNioBuffers != null &&
            cachedNioBuffers.size() < PooledByteBufAllocator.DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK) {
            cachedNioBuffers.offer(nioBuffer);
        }
    }
  • 如果是 small 类型,那么调用 subpage.free() 方法进行释放。
  • 释放normal 类型或者 small 类型已经完成释放了,那么就要释放这个 run 内存块。
  • 调用 collapseRuns(handle) 方法来合并可分配内存块。
  • 调用 insertAvailRun(...) 方法将内存块插入到 runsAvail 中。

3.3.2 collapseRuns

    /**
     * 不断向前和向后合并可用内存块
     */
    private long collapseRuns(long handle) {
        return collapseNext(collapsePast(handle));
    }

    /**
     * 不断向前尝试合并可用内存块
     */
    private long collapsePast(long handle) {
        // 通过循环,不断向前尝试合并
        for (;;) {
            // 得到当前 handle 对应内存块的偏移量 runOffset
            int runOffset = runOffset(handle);
            // 得到当前 handle 对应内存块的大小 runPages
            int runPages = runPages(handle);

            /**
             * 判断释放内存块 run 的前面有没有可用内存块
             */
            long pastRun = getAvailRunByOffset(runOffset - 1);
            if (pastRun == -1) {
                // 前面没有可用内存块,就直接返回
                return handle;
            }

            // 前面可用内存块 run 的偏移量pastOffset
            int pastOffset = runOffset(pastRun);
            // 前面可用内存块 run 的大小 pastPages
            int pastPages = runPages(pastRun);

            //is continuous
            if (pastRun != handle && pastOffset + pastPages == runOffset) {
                // 如果前一个可用内存块 run (pastOffset + pastPages == runOffset),
                // 说明前一个可用内存块和当前释放的内存块是连续的,就需要合并。

                // 删除前一个可用内存块,因为它要被合并
                removeAvailRun(pastRun);
                // 创建新的包含前一个可用内存块的新合并内存块。
                handle = toRunHandle(pastOffset, pastPages + runPages, 0);
            } else {
                return handle;
            }
        }
    }

    /**
     * 不断向后尝试合并可用内存块
     */
    private long collapseNext(long handle) {
        // 通过循环,不断向后尝试合并
        for (;;) {
            int runOffset = runOffset(handle);
            int runPages = runPages(handle);

            /**
             * 判断释放内存块 run 的后面有没有可用内存块
             */
            long nextRun = getAvailRunByOffset(runOffset + runPages);
            if (nextRun == -1) {
                return handle;
            }

            int nextOffset = runOffset(nextRun);
            int nextPages = runPages(nextRun);

            //is continuous
            if (nextRun != handle && runOffset + runPages == nextOffset) {
                // 如果后一个可用内存块 run (runOffset + runPages == nextOffset),
                // 说明后一个可用内存块和当前释放的内存块是连续的,就需要合并。

                // 删除后一个可用内存块,因为它要被合并
                removeAvailRun(nextRun);
                handle = toRunHandle(runOffset, runPages + nextPages, 0);
            } else {
                return handle;
            }
        }
    }

就是查看要释放的内存块,前后有没有连续的可分配内存块,有的话,就合并成一个大的内存块。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,837评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,551评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,417评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,448评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,524评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,554评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,569评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,316评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,766评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,077评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,240评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,912评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,560评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,176评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,425评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,114评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,114评论 2 352

推荐阅读更多精彩内容