Netty内存池源码三 (SizeClasses终结)

Netty-内存池源码三 (SizeClasses终结)

内存池核心类如下:

  • PooledByteBufAllocator
  • PooledUnsafeDirectByteBuf
  • PooledUnsafeDirectByteBuf
  • PoolThreadCache
  • MemoryRegionCache
  • PoolArena
  • SizeClasses 本期介绍
  • PoolChunk
  • LongPriorityQueue
  • LongLongHashMap
  • PoolSubpage

上期 大致了解了 【SizeClasses】 内主要的工作就是 生成 4 张表,供后面的内存分配时使用。 可以更加详细的划分内存规格,减少内存碎片

本期就是对 SizeClasses 类的源码进行一个透彻的分析, 主要要搞清楚 jemalloc4 这样修改有什么好处。

成员变量

首先来看下关键的成员变量

    // 因为sizeClass规格中最小的就是 16B, 因此这里就是定义最小的规格  log2(16) = 4
	// 最小规格的移位
	static final int LOG2_QUANTUM = 4;

	// 因为sizeClasses总表中 是按照每4行为1组,所以 log2(4) = 2
 	// 定义sizeClasses中 4行 为一组,移位为2
    private static final int LOG2_SIZE_CLASS_GROUP = 2;

	// 因为size2idxTab 详细划分查找表中 最大限制size大小为4096,所以这里 log2(4096) = 12
	// 定义最大详细划分查找表的规格限制为4096,移位为12
    private static final int LOG2_MAX_LOOKUP_SIZE = 12;
	
	// 以下7个常量,对应着 sizeClasses总表的 7个列字段
	
	// index
    private static final int INDEX_IDX = 0;
	// log2Group
    private static final int LOG2GROUP_IDX = 1;
	// log2Delta
    private static final int LOG2DELTA_IDX = 2;
	// nDelta
    private static final int NDELTA_IDX = 3;
	// pageSize => isMultiPageSize
    private static final int PAGESIZE_IDX = 4;
	// subpPage
    private static final int SUBPAGE_IDX = 5;
	// log2DeltaLookup
    private static final int LOG2_DELTA_LOOKUP_IDX = 6;
	
	// 定义常量 yes no
    private static final byte no = 0, yes = 1;

	// 一页的大小 默认8KB
    protected final int pageSize;
	
	// 一页的移位 默认13
    protected final int pageShifts;

	// 一chunk的大小 默认16mb
    protected final int chunkSize;

	// 对齐填充 0
    protected final int directMemoryCacheAlignment;
	
	// 总规格数量 默认 76
    final int nSizes;
	
	// subPage的数量 默认39
    int nSubpages;

	// 页倍数的规格数量 默认40
    int nPSizes;

	// 记录最大Subpage的 index 默认是38
    int smallMaxSizeIdx;
	
	// 4K(4096) 
    private int lookupMaxSize;
	
	// sizeClasses 总表
    private final short[][] sizeClasses;
	
	// 规格大小为页的倍数 的表  记录 pageIndex->size
    private final int[] pageIdx2sizeTab;

	// 查询表 记录所有规格的 index=>size 的表
    private final int[] sizeIdx2sizeTab;

    // lookup table used for size <= lookupMaxclass
    // spacing is 1 << LOG2_QUANTUM, so the size of array is lookupMaxclass >> LOG2_QUANTUM
	// 查询表  对容量<= 4K的规格,按照16B 为增量 做个详细的划分。 idx=>index  有256个(4096>>4)
    private final int[] size2idxTab;

由上述常量和变量可看出来,大量的使用了 log2() 来规划这些值。 刚开始阅读的时候,会不习惯,为什么要使用log2() 来表示这些值呢? 用原值不行吗?

我个人觉得大致的原因: 使用log2()来计算,可以化整 ,且保证最终移位回来还是 power of two

例如:某个用户设置一页的大小为8193,那么log2(8193) = 13, 最终移位13后 还是 8192 (1<<13) 。

构造方法

    protected SizeClasses(int pageSize, int pageShifts, int chunkSize, int directMemoryCacheAlignment) {

        // 每一页的大小(默认为 8192 8KB)
        this.pageSize = pageSize;

        // 1左移多少位  为8192  (默认为 13)
        this.pageShifts = pageShifts;

        // 这个chunk的大小 (默认为16777216,即16mb)
        this.chunkSize = chunkSize;

        // 主要是对于Huge这种直接分配的 类型的 数据 将它 对齐为 directMemoryCacheAlignment的倍数(可以忽略该变量,对后面的逻辑没有影响)
        this.directMemoryCacheAlignment = directMemoryCacheAlignment;

        // 16mb => 16,777,216 =>  0000 0001 0000 0000 0000 0000 0000 0000
        // log2(chunkSize) = 31 - 7 = 24
        // 24 + 1 - 4 = 21 => group
        int group = log2(chunkSize) + 1 - LOG2_QUANTUM;

        //generate size classes
        //[index, log2Group, log2Delta, nDelta, isMultiPageSize, isSubPage, log2DeltaLookup]
        // 21 << 2  => 21*4=84
        // sizeClasses = {84}{7}
        sizeClasses = new short[group << LOG2_SIZE_CLASS_GROUP][7];
        
        // 生成sizeClasses表数据
        nSizes = sizeClasses();

        //generate lookup table
        // 下面都是生成 查询表的数据了

        //sizeIdx2sizeTab  size 关联 id的表
        sizeIdx2sizeTab = new int[nSizes];

        //pageIdx2sizeTab  size是页的整数倍的表 (维护关系 id和size)
        pageIdx2sizeTab = new int[nPSizes];

        // 填充sizeIdx2sizeTab 和 pageIdx2sizeTab 这两张表的数据
        idx2SizeTab(sizeIdx2sizeTab, pageIdx2sizeTab);

        //  4096 >> 4 = 256
        size2idxTab = new int[lookupMaxSize >> LOG2_QUANTUM];
        size2idxTab(size2idxTab);
    }

sizeClasses

    // 生成 sizeClasses二维数组表
    private int sizeClasses() {
        int normalMaxSize = -1;

        int index = 0;
        int size = 0;

        // log2Group 和 log2Delta 初始值 4
        int log2Group = LOG2_QUANTUM;
        int log2Delta = LOG2_QUANTUM;

        // ndeltaLimit = 1<<2 = 4  最大增量为4  也就是限制 1个group中有4个size
        int ndeltaLimit = 1 << LOG2_SIZE_CLASS_GROUP;

        //First small group, nDelta start at 0.   第一个 small组 nDetla从0开始
        //first size class is 1 << LOG2_QUANTUM   第一个 size 是 1<<4 = 16

        int nDelta = 0;

        // 生成 第一组的值,放入sizeClasses二维数组中
        // nDelta 从0开始  不超过4  也就是一组有4个size
        while (nDelta < ndeltaLimit) {

            // 给sizeClasses二维数组赋值
            // 第一组 index = 0  log2Group=4  log2Delta=4 nDelta=0  根据这几个值计算并返回size值
            size = sizeClass(index++, log2Group, log2Delta, nDelta++);
        }
        
        // 生成 第二组及后面组的值  
        // log2Group = 4 +2 = 6  log2Group开始为6  
        log2Group += LOG2_SIZE_CLASS_GROUP;

        //All remaining groups, nDelta start at 1.
        //剩下每组的 nDelta从1开始
        while (size < chunkSize) {
            nDelta = 1;
            while (nDelta <= ndeltaLimit && size < chunkSize) {
                size = sizeClass(index++, log2Group, log2Delta, nDelta++);
                // 记录最大的size值
                normalMaxSize = size;
            }

            log2Group++;
            log2Delta++;
        }

        // 断言下 最后生成的 size值是否符合 chunkSize
        //chunkSize must be normalMaxSize
        assert chunkSize == normalMaxSize;

        //return number of size index
        // 返回最大index  默认应该是 75
        return index;
    }

sizeClass

    private int sizeClass(int index, int log2Group, int log2Delta, int nDelta) {
        short isMultiPageSize;

        // log2Delta >= 13 的 其size都是页(8KB)的整倍数
        if (log2Delta >= pageShifts) {
            isMultiPageSize = yes;
        } else {
            // pageSize = 8192  8KB
            int pageSize = 1 << pageShifts;

			// 计算size的公式
            int size = (1 << log2Group) + (1 << log2Delta) * nDelta;

            // 判断是不是 页的整数倍
            isMultiPageSize = size == size / pageSize * pageSize? yes : no;
        }

		// 计算 log2(nDelta)
        int log2Ndelta = nDelta == 0? 0 : log2(nDelta);


        //remove 表示        当 size不是 2的次方的时候 为 yes
        //                   当 size是  2的次方的时候  为no
        // 该值只是当 size >= MAX_LOOKUP_SIZE(4096) 时使用 
        byte remove = 1 << log2Ndelta < nDelta? yes : no;


        // 实际上就是求 log2(size)的  只不过这样更方便而已 当nDelta=4的时候满足
        int log2Size = log2Delta + log2Ndelta == log2Group? log2Group + 1 : log2Group;
		
        if (log2Size == log2Group) {
            remove = yes;
        }

        // 16B 32B ... 1024B 1280B .. 8K ... 28K       13+2 = 15  32768=32KB
        // pageShifts = log2(8K) = 13
        // pageShifts + LOG2_SIZE_CLASS_GROUP = 13 + 2 = 15  为 log2(32K)
        // 得出结论: 若 size < 32K 则是 subPage
        short isSubpage = log2Size < pageShifts + LOG2_SIZE_CLASS_GROUP? yes : no;

        // log2DeltaLookup   当size <= 4096(4K)  值为log2Delta
        //                   当size > 4096 时     值为0
        int log2DeltaLookup = log2Size < LOG2_MAX_LOOKUP_SIZE ||
                              log2Size == LOG2_MAX_LOOKUP_SIZE && remove == no
                ? log2Delta : no;

        // 生成行
        short[] sz = {
                (short) index, (short) log2Group, (short) log2Delta,
                (short) nDelta, isMultiPageSize, isSubpage, (short) log2DeltaLookup
        };

        // 插入数组
        sizeClasses[index] = sz;
        int size = (1 << log2Group) + (nDelta << log2Delta);

        // 判断该行是否是 页的整数倍 若是则 nPSizes+1
        if (sz[PAGESIZE_IDX] == yes) {
            nPSizes++;
        }

        // 判断该行是否是 subPage  若是则 nSubpages + 1 ,smallMaxSizeIdx记录最大的subPage的index
        if (sz[SUBPAGE_IDX] == yes) {
            nSubpages++;
            smallMaxSizeIdx = index;
        }

        // lookupMaxSize 记录 <= 4096(4K)  最大的size值
        if (sz[LOG2_DELTA_LOOKUP_IDX] != no) {
            lookupMaxSize = size;  // 最大4K
        }
        return size;
    }

int log2Ndelta = nDelta == 0? 0 : log2(nDelta);

byte remove = 1 << log2Ndelta < nDelta? yes : no;

int log2Size = log2Delta + log2Ndelta == log2Group? log2Group + 1 : log2Group;

sizeClass函数中 其中上述三行代码 刚开始 我看的时候 一脸懵逼, 完全不知道是什么意思,仔细研究后发现并不难。

  1. 首先 通过上一期我们可知道 size = (1 << log2Group) + (1 << log2Delta) * nDelta 该求size公式可转换成 size = 2^(log2Delta) * (4 + nDelta)

  2. int log2Ndelta = nDelta == 0? 0 : log2(nDelta);

    这行代码实际上不难理解,就是为了计算出 log2(nDelta) 的值。

    我们知道 求log2() 实际上 是向下取整, 比如 log2(5) = 2 , log2(3) = 1 等等 5 ,3这些数不是 POWER OF TWO 时,其 log2值 都是向下取整的。

  3. byte remove = 1 << log2Ndelta < nDelta? yes : no;

    remove 值 判断 1<< log2Ndelta < nDelta 就是为了看此值是否是2的倍数。 而我们清楚 除了第一组之外,nDelta取值范围为 [1,4] ,那么该区间范围内,只有4符合 POWER OF TWO, 也就是每组的最后一个规格。

  4. int log2Size = log2Delta + log2Ndelta == log2Group? log2Group + 1 : log2Group;

    我们知道 nDelta 第一组的取值范围为 [0,3], 除了第一组其它组取值范围为[1,4],

    为了让 size 的结果是 POWER OF TWO (2的次方数), 那么 nDelta 只能取 0 或 4 。

    当nDelta = 0 时 也就是只有第一组的第一个规格满足,此时 size = 1<<log2Group

    当nDelta = 4 时,也就是除了第一组外 其他组的最后一个规格满足,此时size = 1<<(log2Group + 1)

idx2SizeTab

该方法主要就是用来生成 sizeIdx2sizeTab表,与pageIdx2sizeTab 表的。

很简单,没什么可说的。 生成的对应表详细信息, 可以在上一期查看。

    private void idx2SizeTab(int[] sizeIdx2sizeTab, int[] pageIdx2sizeTab) {
        int pageIdx = 0;

        // 循环遍历总表
        for (int i = 0; i < nSizes; i++) {
            short[] sizeClass = sizeClasses[i];
            int log2Group = sizeClass[LOG2GROUP_IDX];
            int log2Delta = sizeClass[LOG2DELTA_IDX];
            int nDelta = sizeClass[NDELTA_IDX];

            // 计算该行 sizeClass 的 size值
            int size = (1 << log2Group) + (nDelta << log2Delta);
            // 填充sizeIdx2sizeTab表
            sizeIdx2sizeTab[i] = size;

            // 如果 isMultiPageSize 是 yes 也就是说 该sizeClass的size值是 页的整数倍的时候 加入pageIdx2sizeTab表中
            if (sizeClass[PAGESIZE_IDX] == yes) {
                pageIdx2sizeTab[pageIdx++] = size;
            }
        }
    }

size2idxTab

生成的size2idxTab 表 可参考上一期。

    private void size2idxTab(int[] size2idxTab) {
        int idx = 0;
        int size = 0;
		
        // 遍历每一个 小于 4096 的规格
        for (int i = 0; size <= lookupMaxSize; i++) {
           	
            // 获取 log2Delta
            int log2Delta = sizeClasses[i][LOG2DELTA_IDX];

            // 该代码可理解成   2^log2Delta/16   
            // 目的是 求出 该规格 可以按照16划分多少份
            int times = 1 << log2Delta - LOG2_QUANTUM;

            // size <= 4096
           // 生成详细的 以16为增量的规格值
            while (size <= lookupMaxSize && times-- > 0) {
                size2idxTab[idx++] = i;
                size = idx + 1 << LOG2_QUANTUM;
            }
        }
    }

size2SizeIdx

该方法 是为了求出接近 用户申请内存大小 的内存规格下标。

当申请内存 <= 4096 时,直接从 size2idxTab 表中获取

当申请内存 > 4096 时,需要计算 group(申请内存所属组的第一个规格的index)+mod(该申请内存在所属组的偏移量)

例如: 用户申请大小为14 ,则返回数组索引值为 0。

    public int size2SizeIdx(int size) {
        if (size == 0) {  // 当请求申请的size为0时, 则返回0
            return 0;
        }
        if (size > chunkSize) { //当请求申请的size大于chunkSize时, 返回最大索引 也就是chunkSize的index
            return nSizes;
        }
        
		// 当前偏移量大于0时,默认该条件不会成立
        if (directMemoryCacheAlignment > 0) {  
            size = alignSize(size);
        }

        // 当size <= 4K 时  从size2idxTab 细粒度表中 直接获取index
        if (size <= lookupMaxSize) {   
            return size2idxTab[size - 1 >> LOG2_QUANTUM];
        }

        // 来到这 就是 size > 4K 的时候
        
        
		// 该表达式是为了将申请内存向上取整(该size的移位),得出该size所属组的移位。
        // -1 是为了避免申请内存大小刚好是 POWER OF TWO(每组的最后一个规格),这样可以让每组的最后一个规格移位与同组其它规格移位相同。 
        // 最终 1<< 移位 = 当前组最后一个规格内存块的size
        
        // 例子1: size = 16KB    2B0100 0000 0000 0000
        //        (size << 1) - 1 = 2B0100 0000 0000 00000 - 1 
        //                        = 2B0011 1111 1111 11111
        // 			log2((size<<1)-1) = 14
        
        // 例子2: size = 13KB    2B0011 0100 0000 0000
        //          (size << 1) - 1 = 2B0011 0100 0000 00000 -1
        //                          = 2B0011 0011 1111 11111
        //          log2((size<<1)-1) = 14
        int x = log2((size << 1) - 1);
		
       	
        /** 
        * 1. 从上得出x是每组最后一个内存块的size的移位。
        *
        * 2. 已知 log2Group = log2Delta +  LOG2_SIZE_CLASS_GROUP
        *  	除了第一组外,每组的最后一个内存块的 nDelta = 4
        *  	将上面两个已知条件代入求size的公式可得:   
        *  	lastSize = 1 << (log2Group + 1)
        *  	两边同时 取log2 得:
        *  	x  = log2Group + 1   
  		*/
		 
         
        // x < LOG2_SIZE_CLASS_GROUP + LOG2_QUANTUM + 1 
        // 将 x = log2Group + 1 代入后得:
     	// log2Group < LOG2_SIZE_CLASS_GROUP + LOG2_QUANTUM 
        // log2Group < 6  时  shift = 0   (第一组)
        // log2Group >= 6 时  shift = log2Group - 5  (第二组及以后)
        // 最终shift 可以通过后面得表达式计算出 该组第一个内存块得index索引  
        int shift = x < LOG2_SIZE_CLASS_GROUP + LOG2_QUANTUM + 1
                ? 0 : x - (LOG2_SIZE_CLASS_GROUP + LOG2_QUANTUM);

        //  该表达式就是计算 该组得第一个内存块的 index
        int group = shift << LOG2_SIZE_CLASS_GROUP;
		
        
        // 下面时计算 mod值 ,最终确定要分配得内存块是属于该组得第几个
        // 计算log2Delta 
        // 当第0组时   log2Delta = 4
        // 第一组及以后 log2Delta = log2Group - 2
        int log2Delta = x < LOG2_SIZE_CLASS_GROUP + LOG2_QUANTUM + 1
                ? LOG2_QUANTUM : x - LOG2_SIZE_CLASS_GROUP - 1;

        //  计算掩码
        // size = (1<<log2Delta) * (4 + nDelta)
        // index = group + mod  
        // group 是该组得第一个内存块索引  
        // mod 是偏移位置    也即是 nDelta - 1
        // size=(LOG2_SIZE_CLASS_GROUP + nDelta -1 + 1)* (1<<log2Delta)
       // 再进一步转换 将1提出来
//size=(LOG2_SIZE_CLASS_GROUP + nDelta -1)*(1<<log2Delta)+(1<<log2Delta)
        
 //LOG2_SIZE_CLASS_GROUP + nDelta-1=(size-(1<<log2Delta))/(1<<log2Delta)
                            
        int deltaInverseMask = -1 << log2Delta;

        // size - 1 是为了申请内存等于内存块size时避免分配到下一个内存块size中
        // & deltaInverseMask 可以理解为 减去 1<<log2Delta 
        // >> log2Delta   可以理解成  除以 1<<log2Delta
        // 此时 就得到 LOG2_SIZE_CLASS_GROUP + nDelta-1
//mod = LOG2_SIZE_CLASS_GROUP + nDelta-1 & (1<< LOG2_SIZE_CLASS_GROUP)-1
  //最终 mod = nDelta - 1
        int mod = (size - 1 & deltaInverseMask) >> log2Delta &
                  (1 << LOG2_SIZE_CLASS_GROUP) - 1;
      //28 + 3 = 31
        return group + mod;
    }

该方法可能看上去比较复杂,晦涩难懂,但是仔细跟着代入公式算一遍,其实很好理解。

page2pageIdxCompute

该方法主要目的就是 根据申请得 页数,来计算该 页数 在 pageIdx2sizeTab 表中得 pageIdx

该方法与 size2SizeIdx实际上计算思想相同,都是使用 group + mod

    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;

        if (floor && pageIdx2sizeTab[pageIdx] > pages << pageShifts) {
            pageIdx--;
        }

        return pageIdx;
    }
万般皆下品,唯有读书高!
原文地址:https://www.cnblogs.com/s686zhou/p/15714858.html