Высокопроизводительный анализ исходного кода Netty ByteBuf

задняя часть Netty

Оригинальная ссылка:nuggets.capable/post/684490…


Тип ByteBuf Netty

  • Pooled (объединение), Unpooled (не объединение)

  • Direct (прямой буфер/вне кучи), Heap (внутри кучи jvm)

  • небезопасный (локальный метод, вызываемый unsafe), безопасный (вообще говоря, это относительно небезопасного, который относится к операции в куче jvm)

Netty по умолчанию сначала будет использовать небезопасную реализацию.


В пуле/не в пуле

Сначала Netty применяет непрерывное пространство в качестве пула ByteBuf. Когда его необходимо использовать, он переходит непосредственно в пул и возвращает его в пул ByteBuf после его использования. Его не нужно применять каждый раз, когда используется ByteBuf. Коэффициент создания объектов вне кучи Время в куче.

Резюме: Роль пулинга заключается в ускорении получения программой объектов для работы и снижении накладных расходов на частое создание/уничтожение.


Вне кучи/в куче (прямой/куча)

Куча относится к данным в JVM, приложения и операции находятся в JVM.

Прямой буфер вне кучи относится к памяти кучи, отличной от jvm, применяемой нативным методом при подаче заявки на память.Эта часть памяти ОС может использоваться напрямую и является физически непрерывными данными.В отличие от памяти в куче, она является логической Преемственность по адресу вместо физической преемственности, и адрес, скорее всего, изменится после gc, а нижний слой будет делать ошибку при чтении данных соответствующего адреса, поэтому обработка JDK заключается в том, что при записи он также необходимо один раз скопировать в прямой буфер (последовательные физические адреса, в частности, в IOUtil#write() ---> Util.getTemporaryDirectBuffer вызывается и помещается во временный буфер вне кучи). на этот раз объект (DirectByteBuf) в java — это только некоторая обработка индекса чтения/записи (память (адрес памяти), смещение (смещение) и т. д.), запись данных/чтение данных — все операции над данными вне кучи через родной .

Суммировать:

  • Если вы используете память вне кучи напрямую, вы можете减少一次堆内复制到堆外的临时缓冲区, поскольку адреса в куче логически непрерывны, а обработка сети или файлов требует непрерывности физических адресов.Если gc выполняется при записи данных, то адреса до и после gc могут меняться, и в итоге os не может быть прочитан , Соответствующие данные вызывают проблему.

  • Целью использования памяти вне кучи является снижение давления gc && в некоторых сценариях, когда все данные не нужно обрабатывать, некоторые операции копирования вне кучи в операции в куче (такие как упомянутая выше переадресация запросов) могут быть опущены. для повышения эффективности.


unsafe

Unsafe — это класс, представленный в sun.misc. С помощью этого класса вы можете напрямую манипулировать памятью с помощью собственных методов, и, конечно же, это повысит эффективность. Упомянутое выше применение и работа с памятью вне кучи выполняются с помощью этой вещи, называемой небезопасной. , , Но чтобы использовать это небезопасно, вы должны быть хорошо знакомы с операциями с памятью, иначе очень легко сделать ошибки, поэтому понятно, почему официальные лица называют это небезопасным.

Резюме: прямое управление памятью, повышение эффективности и использование подверженных ошибкам




Некоторые классы и концепции, используемые пулом

PoolArena, PoolChunk, PoolThreadLocalCache, PoolSubpage, Recycler

  • PoolArena: Значение этапа Арены, как следует из названия, операции в пуле требуют, чтобы этот класс обеспечивал среду
  • PoolChunk: Блок памяти, запрашиваемый в Netty, хранит такую ​​информацию, как chunkSize, собственное смещение и freeSize размера оставшегося пространства.Согласно официальным инструкциям: Чтобы найти размер в блоке (чанке), который может, по крайней мере, удовлетворить запрос, строится полное бинарное дерево, как куча (это максимальная куча, узлы в чанке образуют полное бинарное дерево)
  • PoolThreadLocalCache: локальная переменная потока, сохранение PoolArena -> чанк (-> страница-> подстраница)
  • PoolSubPage: страница в самом нижнем фрагменте
  • Recycle: Как следует из названия, корзина, это абстрактный класс, основная функция которого состоит в том, чтобы получить ByteBuf в корзину из ThreadLocal.

Краткое описание. И PoolThreadLocalCache, и Recycle используют переменные ThreadLocal для уменьшения конкуренции между потоками и повышения эффективности работы.


Несколько важных значений свойств.

maxOrder по умолчанию 11: глубина полного бинарного дерева (корневой узел — это 0-й слой, поэтому объективно существует общее количество слоев maxOrder+1)

pageSize default 8192 (8k): размер по умолчанию страницы нижнего конечного узла полного бинарного дерева выше

pageShifts по умолчанию 13: это логарифм pageSize, 2^pageShifts = pageSize , pageSize по умолчанию равен 8192, поэтому это значение по умолчанию равно 13.

chunkSize по умолчанию равен 16 м (pageSize * 2 ^ maxOrder): это размер каждого фрагмента, который является размером каждого слоя карты фрагментов ниже.

Минимальная единица деления на странице — 16 байт, число 16 очень важно, и есть несколько ключевых вычислений, которые будут использоваться позже.


Тип размера ByteBuf:

  • size < 512 , tiny
  • 512 < size < 8192 , small
  • 8192 < size < 16m , normal
  • 16m < size , huge

Структура чанков

Сумма каждого слоя 16m, и каждый слой представляет собой чанк, он разбит снизу, и каждая страница 8192 (8k), поэтому внизу 2k узлов, конечно, здесь нарисованы не все, subPages все на странице работают на нем.




Приложение памяти ByteBuffer вне кучи/в куче

простой тест

Выполните простой тест, чтобы проверить, сколько времени занимает приложение с памятью вне кучи и приложение с памятью в куче:

	static void nioAllocTest(){
        int num = 10;
        int cnt = 100;
        int size = 256;
        ByteBuffer buf;

        long start1,end1,start2,end2;
        long sum1,sum2;
        for(int i = 0;i<num;i++){
            sum1=sum2=0;
            int j;
            for(j = 0;j<cnt;j++) {
                start1 = System.nanoTime();
                buf = ByteBuffer.allocateDirect(size);
                end1 = System.nanoTime();
                sum1+=(end1-start1);
//                System.out.println("direct 申请时间: "+(end1-start1));

                start2 = System.nanoTime();
                buf = ByteBuffer.allocate(size);
                end2 = System.nanoTime();
//                System.out.println("heap 申请时间: "+(end2-start2));
//                System.out.println("-----");
                sum2+=(end2-start2);
            }
            System.out.println(String.format("第 %s 轮申请 %s 次 %s 字节平均耗时 [direct: %s , heap: %s].",i,j,size,sum1/cnt, sum2/cnt));
        }
    }

Результат:

Среднее время для 100 приложений для 256 байтов в раунде 0 [Direct: 4864, куча: 1616].
Среднее время для 100 приложений по 256 байт в первом раунде [direct: 5763, heap: 1641].
Второй раунд из 100 приложений для 256 байт среднего времени [direct: 4771 , heap: 1672].
Среднее время для 100 приложений по 256 байт в третьем раунде [direct: 4961, heap: 883].
Четвертый раунд из 100 приложений для 256 байт среднего времени [direct: 3556 , heap: 870].
Среднее время, затрачиваемое на пятый раунд применения 100 раз по 256 байт [direct: 5159, heap: 726].
Среднее время для 100 приложений по 256 байт в шестом раунде [direct: 3739, heap: 843].
Среднее время, затрачиваемое на седьмой раунд применения 100 раз по 256 байт [direct: 3910, heap: 221].
8-й раунд приложения 100 раз среднее время 256 байт [direct: 2191 , heap: 590].
Среднее время на 100 заявок на 256 байт в 9-м раунде [direct: 1624, heap: 615].

Видно, что время применения прямой памяти off-heap существенно больше, чем время применения jvm-кучи, а времязатратность здесь в несколько раз (количество тестов мало, может быть не точно, интересовались студенты можете протестировать больший / меньший размер, вы можете найти что-то «интересное»).


в пуле/не в пуле

простой тест

Проведите простой тест, чтобы проверить эффект объединения

	static void nettyPooledTest(){
        try {
            int num = 10;
            int cnt = 100;
            int size = 8192;
            ByteBuf direct1, direct2, heap1, heap2;

            long start1, end1, start2, end2, start3, end3, start4, end4;
            long sum1, sum2, sum3, sum4;
            for (int i = 0; i<num; i++) {
                sum1 = sum2 = sum3 = sum4 = 0;
                int j;
                for (j = 0; j<cnt; j++) {

                    start1 = System.nanoTime();
                    direct1 = PooledByteBufAllocator.DEFAULT.directBuffer(size);
                    end1 = System.nanoTime();
                    sum1 += (end1-start1);

                    start2 = System.nanoTime();
                    direct2 = UnpooledByteBufAllocator.DEFAULT.directBuffer(size);
                    end2 = System.nanoTime();
                    sum2 += (end2-start2);

                    start3 = System.nanoTime();
                    heap1 = PooledByteBufAllocator.DEFAULT.heapBuffer(size);
                    end3 = System.nanoTime();
                    sum3 += (end3-start3);

                    start4 = System.nanoTime();
                    heap2 = UnpooledByteBufAllocator.DEFAULT.heapBuffer(size);
                    end4 = System.nanoTime();
                    sum4 += (end4-start4);

                    direct1.release();
                    direct2.release();
                    heap1.release();
                    heap2.release();
                }
                System.out.println(String.format("Netty 第 %s 轮申请 %s 次 [%s] 字节平均耗时 [direct.pooled: [%s] , direct.unpooled: [%s] , heap.pooled: [%s] , heap.unpooled: [%s]].", i, j, size, sum1/cnt, sum2/cnt, sum3/cnt, sum4/cnt));
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally {

        }
    }

Окончательный результат вывода:

Приложение Netty раунда 0 для 100 раз [8192] байт среднего времени [direct.pooled: [1784931] , direct.unpooled: [105310] , heap.pooled: [202306] , heap.unpooled: [23317]].
Первый раунд приложения Netty 100 раз [8192] байт среднего времени [direct.pooled: [12849] , direct.unpooled: [15457] , heap.pooled: [12671] , heap.unpooled: [12693]].
Среднее время второго раунда запросов Netty на 100 [8192] байт [direct.pooled: [13589] , direct.unpooled: [14459] , heap.pooled: [18783] , heap.unpooled: [13803]].
Приложение Netty 3-го раунда 100 раз [8192] Среднее время в байтах [direct.pooled: [10185] , direct.unpooled: [11644] , heap.pooled: [9809] , heap.unpooled: [12770]].
Среднее время четвертого раунда запросов Netty на 100 [8192] байтов [direct.pooled: [15980] , direct.unpooled: [53980] , heap.pooled: [5641] , heap.unpooled: [12467]].
Netty Round 5 100 [8192] Байт Среднее время [Direct.pooled: [4903], Direct. Проволочен: [34215], Heap.pooled: [6659], Heap.unpooled: [12311]].
6-й раунд Netty Netty на 100 [8192] в среднем времени [8192].
Седьмой раунд подачи заявки Netty на 100 раз [8192] байт среднего времени [direct.pooled: [2578] , direct.unpooled: [4750] , heap.pooled: [3904] , heap.unpooled: [255689]].
8-й раунд приложения Netty для 100 [8192] байт среднего времени [direct.pooled: [1855] , direct.unpooled: [3492] , heap.pooled: [37822] , heap.unpooled: [3983]].
9-й раунд подачи заявки Netty на 100 раз [8192] байт среднего времени [direct.pooled: [1932] , direct.unpooled: [2961] , heap.pooled: [1825] , heap.unpooled: [6098]].

Из этого видно, что после ByteBuf Pooled эффективность приложения все еще относительно очевидна. Поэтому, если вы часто обращаетесь за памятью вне кучи, это снизит производительность сервера.В это время будет раскрыт эффект объединения.Пулинг нужно применять только для достаточно большой памяти в начале, а последующее получение объекты берутся только из пула, используя After return the Pool, не нужно каждый раз обращаться отдельно, экономя время подачи заявки на место из-за пределов кучи




Конкретная реализация ByteBuf

Вот наиболее важный из них, на мой взгляд, который также используется netty по умолчанию:PooledUnsafeDirectByteBuf, мы также начинаем с его приложения PooledByteBufAllocator.DEFAULT.directBuffer() .

Создать/Получить/Повторно использовать -- new() && выделить && get()/reuse()

Следующее вводится из PooledByteBufAllocator.DEFAULT.directBuffer()

  // 到第一个要分析的方法
  protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
      // 从 threadlLocal 获取一个线程本地缓存池
      PoolThreadCache cache = (PoolThreadCache)this.threadCache.get();
      // 这个缓存池包含 heap 和 direct 两种, 获取直接缓存池
      PoolArena<ByteBuffer> directArena = cache.directArena;
      Object buf;
      if (directArena != null) {
        buf = directArena.allocate(cache, initialCapacity, maxCapacity); // 这里往下 -- 1
      } else {
        // 如果没有堆外缓存池, 直接申请堆外的 ByteBuf, 优先使用 unsafe
        buf = PlatformDependent.hasUnsafe() ? UnsafeByteBufUtil.newUnsafeDirectByteBuf(this, initialCapacity, maxCapacity) : new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
      }

      return toLeakAwareBuffer((ByteBuf)buf);
    }

  // 1  directArena.allocate(cache, initialCapacity, maxCapacity);
  PooledByteBuf<T> allocate(PoolThreadCache cache, int reqCapacity, int maxCapacity) {
      // newByteBuf(maxCapacity); 有两种实现, directArena 和 heapArena
      // Pool 的为在 recycle 中重用一个 ByteBuf
      PooledByteBuf<T> buf = newByteBuf(maxCapacity); // -- 2
      allocate(cache, buf, reqCapacity); // -- 7
      return buf;
    }
	
  // 2 newByteBuf(maxCapacity)
  protected PooledByteBuf<ByteBuffer> newByteBuf(int maxCapacity) {
      // 优先使用 PooledUnsafeDirect
      if (HAS_UNSAFE) {
        // PooledUnsafeDirect
        return PooledUnsafeDirectByteBuf.newInstance(maxCapacity); // -- 3
      } else {
        // PooledDirect
        return PooledDirectByteBuf.newInstance(maxCapacity);
      }
    }

  // 3 PooledUnsafeDirectByteBuf.newInstance
  static PooledUnsafeDirectByteBuf newInstance(int maxCapacity) {
      // 从用于回收的 ThreadLocal 中获取一个 ByteBuf
      PooledUnsafeDirectByteBuf buf = RECYCLER.get();	// -- 4
      // 重置 ByteBuf 的下标等
      buf.reuse(maxCapacity);	// -- 6
      return buf;
    }

  // 4 Recycler.get()
  public final T get() {
      if (maxCapacityPerThread == 0) {
        return newObject((Handle<T>) NOOP_HANDLE);
      }
      // 每个线程都有一个栈
      Stack<T> stack = threadLocal.get();
      // 弹出一个 handle
      DefaultHandle<T> handle = stack.pop();
      // 如果 stack 中没有 handle 则新建一个 
      if (handle == null) {
        handle = stack.newHandle();
        // newObject 由调用者实现, 不同的 ByteBuf 创建各自不同的 ByteBuf, 需要由创建者实现
        // handle.value is ByteBuf, 从上面跟下来, 所以这里是 PooledUnsafeDirectByteBuf
        handle.value = newObject(handle); // -- 5
      }
      // 返回一个 ByteBuf
      return (T) handle.value;
    }
		
  // 5 Stack.pop() , 从栈中取出一个 handle
  DefaultHandle<T> pop() {
      int size = this.size;
      if (size == 0) {
        if (!scavenge()) {
          return null;
        }
        size = this.size;
      }
      size --;
      // 取出栈最上面的 handle
      DefaultHandle ret = elements[size];
      elements[size] = null;
      if (ret.lastRecycledId != ret.recycleId) {
        throw new IllegalStateException("recycled multiple times");
      }
      // 重置这个 handle 的信息
      ret.recycleId = 0;
      ret.lastRecycledId = 0;
      this.size = size;
      return ret;
    }

  // 6 重用 ByteBuf 之前需要重置一下之前的下标等
  final void reuse(int maxCapacity) {
      maxCapacity(maxCapacity);
      setRefCnt(1);
      setIndex0(0, 0);
      discardMarks();
    }	

На приведенных выше шагах с 1 по 6 арена вне кучи получается из PoolThreadLocalCache, а локальный стек ByteBuf получается из RECYCLE в соответствии с требуемым размером, ByteBuf извлекается из стека, а индексы чтения и записи ByteBuf сбрасываются и т. д.


Применить - память / пул / кусок / страница / подстраница /

Говоря об этом, даже если второй шаг закончен, следующий шаг - седьмой шаг.

PooledByteBuf<T> allocate(PoolThreadCache cache, int reqCapacity, int maxCapacity) {
      // newByteBuf(maxCapacity); 有两种实现, directArena 和 heapArena
      // Pool 的为在 recycle 中重用一个 ByteBuf
      PooledByteBuf<T> buf = newByteBuf(maxCapacity); // -- 2
      allocate(cache, buf, reqCapacity); // -- 7
      return buf;
    }

Как было сказано выше, из локального стека потока RECYCLE был получен ByteBuf, а индексы чтения и записи были сброшены.Следующий пункт является ключевым.Продолжаем следить за кодом.

	// allocate(cache, buf, reqCapacity); -- 7
	// 这一段都很重要,代码复制比较多, normal(>8192) 和 huge(>16m) 的暂时不做分析
	private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) 	{
    		// 计算应该申请的大小
        final int normCapacity = normalizeCapacity(reqCapacity); // -- 8

        // 申请的大小是否小于一页 (默认8192) 的大小
        if (isTinyOrSmall(normCapacity)) { // capacity < pageSize
            int tableIdx;
            PoolSubpage<T>[] table;
            // reqCapacity < 512 
            boolean tiny = isTiny(normCapacity);
            if (tiny) { // < 512 is tiny
                // 申请 tiny 容量的空间
                if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {
                    return;
                }
                // 计算属于哪个子页, tiny 以 16B 为单位
                tableIdx = tinyIdx(normCapacity);
                table = tinySubpagePools;
            } else {
                //8192 >  reqCapacity >= 512 is small
                // small 以 1024为单位
                if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) {
                    return;
                }
                tableIdx = smallIdx(normCapacity);
                table = smallSubpagePools;
            }

            // head 指向自己在 table 中的位置的头
            final PoolSubpage<T> head = table[tableIdx];

            /**
             * Synchronize on the head. This is needed as {@link PoolChunk#allocateSubpage(int)} and
             * {@link PoolChunk#free(long)} may modify the doubly linked list as well.
             */
            synchronized (head) {
                final PoolSubpage<T> s = head.next;
                // 这里判断是否已经添加过 subPage
                // 添加过的话, 直接在该 subPage 上面进行操作, 记录标识位等
                if (s != head) {
                    assert s.doNotDestroy && s.elemSize == normCapacity;
                    // 在 subPage 的 bitmap 中的下标
                    long handle = s.allocate();
                    assert handle >= 0;
                    // 用 已经初始化过的 bytebuf 初始化 subPage 中的信息
                    s.chunk.initBufWithSubpage(buf, handle, reqCapacity);
                    // 计数
                    incTinySmallAllocation(tiny);
                    return;
                }
            }
          
            // 第一次创建该类型大小的 ByteBuf, 需要创建一个subPage
            synchronized (this) {
                allocateNormal(buf, reqCapacity, normCapacity);
            }

            // 增加计数
            incTinySmallAllocation(tiny);
            return;
        }
  }

Рассчитайте размер ByteBuf, который следует применить для

    // 8 以下代码是在 normalizeCapacity(reqCapacity) 中
    // 如果 reqCapacity >= 512 ,则使用 跟hashMap 相同的扩容算法
    // reqCapacity < 512(tiny类型) 则将 reqCapacity 变成 16 的倍数	
	if (!isTiny(reqCapacity)) { 
    // 是不是很熟悉, 有没有印象 HashMap 的扩容, 找一个不小于原数的2的指数次幂大小的数
    int normalizedCapacity = reqCapacity;
    normalizedCapacity --;
    normalizedCapacity |= normalizedCapacity >>>  1;
    normalizedCapacity |= normalizedCapacity >>>  2;
    normalizedCapacity |= normalizedCapacity >>>  4;
    normalizedCapacity |= normalizedCapacity >>>  8;
    normalizedCapacity |= normalizedCapacity >>> 16;
    normalizedCapacity ++;

    //
    if (normalizedCapacity < 0) {
      normalizedCapacity >>>= 1;
    }
    assert directMemoryCacheAlignment == 0 || (normalizedCapacity & directMemoryCacheAlignmentMask) == 0;

    return normalizedCapacity;
  }

	// reqCapacity < 512
	// 已经是16的倍数,不做操作
	if ((reqCapacity & 15) == 0) {
    	return reqCapacity;
  	}
	// 不是16的倍数,转化为16的倍数
	return (reqCapacity & ~15) + 16; 

так какsmall 和 tinyСходства еще много, поэтому выбираемtinyГоворящий

// 申请 tiny 容量的空间
if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {
  return;
}
// 计算属于哪个子页, tiny 以 16B 为单位
tableIdx = tinyIdx(normCapacity);
table = tinySubpagePools;

// head 指向自己在 table 中的位置的头
final PoolSubpage<T> head = table[tableIdx];

Я вижу здесь tinySubpagePools, и имя должно быть там, где хранится tinySubPage.Если вы проследите его, вы увидите, что tinySubPage инициализируется в конструкторе

tinySubpagePools = newSubpagePoolArray(numTinySubpagePools);
// 初始化 32 种类型的 subPage 的 head , 这里是记录 head
for (int i = 0; i < tinySubpagePools.length; i ++) {
  tinySubpagePools[i] = newSubpagePoolHead(pageSize);
}
// 512 / 16 = 32
static final int numTinySubpagePools = 512 >>> 4;

numTinySubpagePools, это статическая переменная, 512 - это граничная точка малого и крошечного, 512 >>> 4 = 32, почему беззнаковый сдвиг вправо 4, помните, что упомянутая выше базовая единица подстраницы выделила ее, распределение базовой единицы подстраницы 16 байт, поэтому здесь вычисляется 16 от 16 до 512 единиц общего количества размеров типов ByteBuf, tinySubpagePools -> [16,32,48 .... 512], указанный выше tinyIdx (int normCapacity) вычисляется ByteBuf к какому типу принадлежит и приобретает индексы типа ByteBuf tinySubpagePools, в последующем можно получить пул соответствующих индексов головы, конструктор инициализирует всю голову, фактическое приложение, это не с головой в соответствии с индексом для применения, но будет новое дополнение Subpage , затем двусвязный список с этим заголовком.в соответствии с приведенным выше порядком кода, следующий шаг к poolSubPage (инициализация или выделение)


Роль некоторых полей в subPage

  final PoolChunk<T> chunk;
  // 当前 subPage 所处的 Page 节点下标
  private final int memoryMapIdx;
  // 当前子页的 head 在 该 chunk 中的偏移值, 单位为 pageSize(default 8192)
  private final int runOffset;
  // default 8192
  private final int pageSize;
  // 默认 8 个 long 的字节长度, long是64位, 8*64 = 512, 512 * 16(subPage最低按照16字节分配) = 8192(one default page)
  // 意思是将 一个page分为 512 个 16byte, 每一个 16byte 用一位(bit)来标记是否使用, 一个long有64bit, 所以一共需要 512 / 64 = 8个long类型来作为标记位
  private final long[] bitmap;
  // 这个是指一个 Page 中最多可以存储多少个 elemSize 大小 ByteBuf
  // maxNumElems = pageSize / elemSize
  private int maxNumElems;
  // 已经容纳多少个 elemSize 大小的 ByteBuf
  private int numAvail;
  // 这个是记录真正能使用到的 bit 的length, 因为你不可能每个 page 中的 elemSize 都是16,肯定是有其他大小的, 在 PoolSubPage 的 init 方法中可以看到: bitmapLength = maxNumElems >>> 6; 
  private int bitmapLength;
  // 所以初始化方法 init(), 只初始化 bitmapLength 个 long 类型
	/**
	* for (int i = 0; i < bitmapLength; i ++) {
  *             bitmap[i] = 0;
  * }
  */          

Подводя итог, для страницы размером 8192 сначала вычисляем максимальное количество байтовых массивов такого размера, которые можно разместить согласно входящему размеру (байтовые массивы используются вне кучи) maxNumElems, а затем вычисляем максимальное количество байтов которое может быть размещено в соответствии с максимальным числом, которое может быть размещено.Сколько чисел типа long используется в качестве маркера bitmapLength, и, наконец, битовая карта инициализируется.Можно видеть, что битовая карта - это позиция, которая использовалась в отмеченная страница (в блоках по 16 байт).

В PoolSubPage также есть очень важный метод: toHandle(); Функция этого метода состоит в том, чтобы объединить индексы узла memoryMapIdx и bitmapIdx и использовать для записи длинный тип. С помощью этого значения дескриптора вы можете получить соответствующий узел ( по memoryMapIdx) и соответствующее смещение позиции под узлом (страницей) (то есть bitmapIdx * 16)

  private long toHandle(int bitmapIdx) {
        // 后续会用 (int)handle 将这个 handle 值变回为 memoryMapIdx , 即所属节点下标
        return 0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx;
  }

После введения значения полей subPage продолжайте следовать приведенному выше коду:

Этот фрагмент кода обрабатывается после получения головного узла, соответствующего индексу, в соответствии с размером приложения. на исходной подстранице без использования вызова следующего allocateNormal(buf, reqCapacity, normCapacity); для выделения новой подстраницы

  synchronized (head) {
    final PoolSubpage<T> s = head.next;
    // 这里判断是否已经添加过 subPage
    // 添加过的话, 直接在该 subPage 上面进行操作, 记录标识位等
    if (s != head) {
      assert s.doNotDestroy && s.elemSize == normCapacity;
      // 在 subPage 的 bitmap 中的下标 && 节点下标
      long handle = s.allocate();
      assert handle >= 0;
      // 用已经初始化过的 bytebuf 更新 subPage 中的信息
      s.chunk.initBufWithSubpage(buf, handle, reqCapacity);
      // 计数
      incTinySmallAllocation(tiny);
      return;
    }
  }

Метод initBufWithSubpage можно проследить, чтобы увидеть:

buf.init(
        this, handle,
        runOffset(memoryMapIdx) + (bitmapIdx & 0x3FFFFFFF) * subpage.elemSize + offset,
            reqCapacity, subpage.elemSize, arena.parent.threadCache());

runOffset(memoryMapIdx): memoryMapIdx — индекс узла, runOffset представляет смещение узла в чанке, в 8192 единицах Смещение узла (bitmapIdx & 0x3FFFFFFF) * subpage.elemSize: это смещение представляет собой смещение индекса bitmapIdx в subPage. offset: представляет собой смещение самого чанка

Сумма этих трех смещений является конкретным значением смещения нижнего индекса, представленного bitmapIdx во всем пуле кэша.




Эта статья принадлежит личному пониманию автора, и я надеюсь, что вы сможете указать на возможные ошибки в написании.