Pool de memória Netty - uma explicação detalhada do princípio de PoolChunk, veja aqui ~

PoolChunkNetty é uma parte importante do pool de memória, sua principal função é manter um grande bloco de memória, quando a necessidade de aplicar mais de 8KB de memória, ela o fará a partir da PoolChunkaquisição de. Este artigo PoolChunkexplicará primeiro a estrutura geral, depois explicará a função de seus principais atributos e, finalmente, PoolChunkcomo realizar a aplicação e a liberação de grandes blocos de memória da perspectiva do código-fonte .

1. Estrutura geral do PoolChunk

PoolChunkO tamanho da memória do aplicativo padrão é 16 M. Estruturalmente, ele organizará a memória de 16 MB em uma árvore binária balanceada. O tamanho da memória representado por cada nó de cada camada da árvore binária é igual, e cada camada do nó representa A soma do tamanho total da memória é 16M, o número total de camadas de toda a árvore binária é 12 e o número da camada começa em 0. O diagrama de estrutura é o seguinte:

Em relação à imagem acima, precisamos principalmente explicar os seguintes pontos:

  • A memória total ocupada por um PoolChunk é de 16 M. Ele dividirá a memória de 16 MB de acordo com o nível atual da árvore binária. Por exemplo, a primeira camada irá dividi-la em dois 8 MB, a segunda camada irá dividi-la em quatro 4 MB, etc. A árvore binária tem no máximo 12 níveis, portanto o tamanho da memória de cada nó folha é de 8 KB, ou seja, a memória que o PoolChunk pode alocar é de no mínimo 8 KB e no máximo 16 MB;
  • Pode-se ver que há 2 ^ 11 = 2048 nós folha na árvore binária na figura, então o número de nós em toda a árvore é 4095. PoolChunk agrupou esses 4095 nós em uma matriz com um comprimento de 4096. O primeiro bit armazena 0, o segundo e o terceiro armazenam 1, e o quarto e sétimo bits armazenam 2 e assim por diante, o todo é realmente Ou seja, a árvore binária representada pelo número da camada é armazenada em um array. A matriz aqui é o depthMap à esquerda. Por meio dessa árvore binária, o número de camadas pode ser obtido rapidamente por meio do subscrito. Por exemplo, o valor da posição 2048 é 11, o que significa que está na 11ª camada da árvore binária. A estrutura do depthMap é mostrada abaixo:

  • Em cada nó da árvore binária na figura, marcamos um número para o tamanho da memória representado pelo nó atual. Esse número realmente representa o tamanho da memória que o nó atual pode alocar, como 0 para 16M, 1 para 8M, etc. . Esses números são armazenados pelo memoryMap, que representa o tamanho da memória alocável representada por cada nó na árvore binária, e sua estrutura de dados é exatamente a mesma que a do depthMap. Na figura, o tamanho da memória alocável representada por cada nó pai é igual à soma dos dois nós filhos. Se a memória de um nó filho foi alocada, o nó será marcado como 12, indicando que ele foi alocado, e seus O nó pai será atualizado com o valor de outro nó filho, indicando que a memória que o nó pai pode alocar é a soma da memória fornecida por seus dois nós filhos;
  • Para o processo geral de aplicação e liberação de memória do PoolChunk, descreveremos o aplicativo para 9 KB de memória:
    1. Em primeiro lugar, 9 KB = 9126 é expandido para um expoente maior que o primeiro 2, ou seja, 2 << 13 = 16384. Como o nó folha 8 KB = 2 << 12, o número correspondente de camadas é 11, portanto, o número de camadas onde 16384 está localizado É 10, o que significa que você pode encontrar um nó não alocado da 10ª camada do pool de memória;
    2. Depois que o nó de destino estiver na 10ª camada, a comparação começará a partir do nó principal. Se o valor armazenado pelo nó principal for menor que 10, isso significa que ele tem memória suficiente para alocar a memória de destino, e então O nó filho esquerdo é comparado com 10, se o nó filho esquerdo for maior que 10 (geralmente o nó filho esquerdo foi alocado neste momento, e seu valor é 12, então será maior que 10), então o nó filho direito será 10 para comparação, neste momento o nó filho direito é definitivamente menor que 10, então a comparação acima continuará a partir do nó filho esquerdo do nó filho direito;
    3. Ao comparar a um determinado momento, o número de um nó é igual a 10, isso significa que este local é o bloco de memória que precisamos, então ele será marcado como 12, e então retrocedendo recursivamente, a memória representada por seu nó pai O valor é atualizado para o valor de outro nó filho;

  • Em relação à alocação de memória, a última questão que precisa ser explicada aqui é que através do método de cálculo acima, podemos encontrar um nó como nosso nó de destino a ser alocado. Nesse momento, precisamos retornar o endereço inicial e o comprimento da memória representada por este nó. . Uma vez que temos apenas o valor do endereço da memória de 16M solicitada por todo o PoolChunk, o deslocamento do nó em relação ao endereço inicial de todo o bloco de memória pode ser calculado por meio do número da camada do nó de destino e do número de nós na camada. Desta forma, o valor do endereço inicial do nó pode ser obtido; sobre o comprimento de memória ocupado pelo nó, a sensação intuitiva pode ser entendida como um mapeamento, como 11 para comprimento de 8 KB, 10 para comprimento de 16 KB e assim por diante. Obviamente, o cálculo do endereço inicial e do deslocamento aqui não é implementado diretamente por PoolChunk por meio desse algoritmo, mas por meio de operações de bits mais eficientes.

2. O papel dos principais atributos do PoolChunk

Ao ler o código-fonte do pool de memória Netty, acredito que a maioria dos leitores ficará confusa com seus vários atributos complexos, o que torna difícil de entender. Aqui listamos seus atributos separadamente para facilitar os leitores a entender a função de cada atributo mais rapidamente ao ler o código-fonte.

// netty内存池总的数据结构,该类我们后续会对其进行讲解
final PoolArena<T> arena;
// 当前申请的内存块,比如对于堆内存,T就是一个byte数组,对于直接内存,T就是ByteBuffer,
// 但无论是哪种形式,其内存大小都默认是16M
final T memory;
// 指定当前是否使用内存池的方式进行管理
final boolean unpooled;
// 表示当前申请的内存块中有多大一部分是用于站位使用的,整个内存块的大小是16M+offset,默认该值为0
final int offset;
// 存储了当前代表内存池的二叉树的各个节点的内存使用情况,该数组长度为4096,二叉树的头结点在该数组的
// 第1号位,存储的值为0;两个一级子节点在该数组的第2号位和3号位,存储的值为1,依次类推。二叉树的叶节点
// 个数为2048,因而总节点数为4095。在进行内存分配时,会从头结点开始比较,然后比较左子节点,然后比较右
// 子节点,直到找到能够代表目标内存块的节点。当某个节点所代表的内存被申请之后,该节点的值就会被标记为12,
// 表示该节点已经被占用
private final byte[] memoryMap;
// 这里depthMap存储的数据结构与memoryMap是完全一样的,只不过其值在初始化之后一直不会发生变化。
// 该数据的主要作用在于通过目标索引位置值找到其在整棵树中对应的层数
private final byte[] depthMap;
// 这里每一个PoolSubPage代表了二叉树的一个叶节点,也就是说,当二叉树叶节点内存被分配之后,
// 其会使用一个PoolSubPage对其进行封装
private final PoolSubpage<T>[] subpages;

// 其值为-8192,二进制表示为11111111111111111110000000000000,它的后面0的个数正好为12,而2^12=8192,
// 因而将其与用户希望申请的内存大小进行“与操作“,如果其值不为0,就表示用户希望申请的内存在8192之上,从而
// 就可以快速判断其是在通过PoolSubPage的方式进行申请还是通过内存计算的方式。
private final int subpageOverflowMask;
// 记录了每个业节点内存的大小,默认为8192,即8KB
private final int pageSize;
// 页节点所代表的偏移量,默认为13,主要作用是计算目标内存在内存池中是在哪个层中,具体的计算公式为:
// int d = maxOrder - (log2(normCapacity) - pageShifts);
// 比如9KB,经过log2(9KB)得到14,maxOrder为11,计算就得到10,表示9KB内存在内存池中为第10层的数据
private final int pageShifts;
// 默认为11,表示当前你最大的层数
private final int maxOrder;
// 记录了当前整个PoolChunk申请的内存大小,默认为16M
private final int chunkSize;
// 将chunkSize取2的对数,默认为24
private final int log2ChunkSize;
// 指定了代表叶节点的PoolSubPage数组所需要初始化的长度
private final int maxSubpageAllocs;

// 指定了某个节点如果已经被申请,那么其值将被标记为unusable所指定的值
private final byte unusable;
// 对创建的ByteBuffer进行缓存的一个队列
private final Deque<ByteBuffer> cachedNioBuffers;

// 记录了当前PoolChunk中还剩余的可申请的字节数
private int freeBytes;

// 在Netty的内存池中,所有的PoolChunk都是由当前PoolChunkList进行组织的,
// 关于PoolChunkList和其前置节点以及后置节点我们会在后续进行讲解,本文主要专注于PoolChunk的讲解
PoolChunkList<T> parent;
// 在PoolChunkList中当前PoolChunk的前置节点
PoolChunk<T> prev;
// 在PoolChunkList中当前PoolChunk的后置节点
PoolChunk<T> next;

3. Implementação do código fonte

Em relação PoolChunkàs funções, explicamos principalmente o processo de alocação e recuperação de memória.

3.1 Alocação de memória

PoolChunkA alocação de memória está principalmente em seu allocate()método, e a descrição geral da alocação foi explicada antes, portanto, não vou repeti-la aqui, vamos inserir diretamente seu código-fonte para ler:

boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
  final long handle;
  // 这里subpageOverflowMask=-8192,通过判断的结果可以看出目标容量是否小于8KB。
  // 在下面的两个分支逻辑中,都会返回一个long型的handle,一个long占8个字节,其由低位的4个字节和高位的
  // 4个字节组成,低位的4个字节表示当前normCapacity分配的内存在PoolChunk中所分配的节点在整个memoryMap
  // 数组中的下标索引;而高位的4个字节则表示当前需要分配的内存在PoolSubPage所代表的8KB内存中的位图索引。
  // 对于大于8KB的内存分配,由于其不会使用PoolSubPage来存储目标内存,因而高位四个字节的位图索引为0,
  // 而低位的4个字节则还是表示目标内存节点在memoryMap中的位置索引;
  // 对于低于8KB的内存分配,其会使用一个PoolSubPage来表示整个8KB内存,因而需要一个位图索引来表示目标内存
  // 也即normCapacity会占用PoolSubPage中的哪一部分的内存。
  if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize
    // 申请高于8KB的内存
    handle = allocateRun(normCapacity);
  } else {
    // 申请低于8KB的内存
    handle = allocateSubpage(normCapacity);
  }

  // 如果返回的handle小于0,则表示要申请的内存大小超过了当前PoolChunk所能够申请的最大大小,也即16M,
  // 因而返回false,外部代码则会直接申请目标内存,而不由当前PoolChunk处理
  if (handle < 0) {
    return false;
  }

  // 这里会从缓存的ByteBuf对象池中获取一个ByteBuf对象,不存在则返回null
  ByteBuffer nioBuffer = cachedNioBuffers != null ? cachedNioBuffers.pollLast() : null;
  // 通过申请到的内存数据对获取到的ByteBuf对象进行初始化,如果ByteBuf为null,则创建一个新的然后进行初始化
  initBuf(buf, nioBuffer, handle, reqCapacity);
  return true;
}

Pode-se ver que para a alocação de memória, é avaliado principalmente se é maior que 8 KB. Se for maior que 8 KB, será alocado diretamente na árvore binária de PoolChunk. Se for menor que 8 KB, será aplicado diretamente para uma memória de 8 KB e, em seguida, 8 KB de memória Entregue a uma subpágina Pool para manutenção. Em relação ao princípio de implementação do PoolSubpage, iremos explicá-lo mais tarde. Aqui, apenas o explicamos resumidamente para ajudar os leitores a entender o conceito de índice de bitmap. Quando solicitamos 8 KB de memória da árvore binária do PoolChunk, ele será entregue a uma subpágina PoolSubpage para manutenção. Em PoolSubpage, ele vai dividir todo o tamanho do bloco de memória em uma série de tamanhos de 16 bytes, aqui é de 8 KB, ou seja, será dividido em 512 = 8 KB / 16 bytes de cópias. Para identificar se cada cópia está ocupada, PoolSubpage usa uma longa matriz para representá-la.O nome da matriz é bitmap, então a chamamos de matriz de bitmap. Para indicar se 512 dados estão ocupados ou não, um long tem apenas 64 bytes, então 8 = 512/64 longs são necessários aqui para representar, então o array long é usado aqui em vez de um único campo longo. Mas o leitor deve ter descoberto que os 32 bits altos do identificador aqui são um valor inteiro e o bitmap que descrevemos é uma matriz longa, então como um inteiro indica que a memória solicitada atualmente é o elemento de número na matriz de bitmap? E o número do inteiro 64 bytes no elemento. Na verdade, isso é representado pelos 67 bits baixos do número inteiro. O 64º e o 67º bits são usados ​​para indicar o número de elementos longos na matriz de bitmap atualmente ocupada e os 1 64 bits são usados ​​para indicar o número de elementos longos no elemento longo. Quantos estão ocupados pelo aplicativo atual. Portanto, apenas um identificador de inteiro longo é necessário para indicar a posição da memória aplicada atualmente em todo o pool de memória e a posição na Subpágina Pool. Aqui, primeiro lemos allocateRun()o código-fonte do método:

private long allocateRun(int normCapacity) {
  // 这里maxOrder为11,表示整棵树最大的层数,log2(normCapacity)会将申请的目标内存大小转换为大于该大小的
  // 第一个2的指数次幂数然后取2的对数的形式,比如log2(9KB)转换之后为14,这是因为大于9KB的第一个2的指数
  // 次幂为16384,将其取2的对数后为14。pageShifts默认为13,这里整个表达式的目的就是快速计算出申请目标
  // 内存(normCapacity)需要对应的层数。
  int d = maxOrder - (log2(normCapacity) - pageShifts);
  // 通过前面讲的递归方式从先父节点,然后左子节点,接着右子节点的方式依次判断其是否与目标层数相等,
  // 如果相等,则会将该节点所对应的在memoryMap数组中的位置索引返回
  int id = allocateNode(d);
  // 如果返回值小于0,则说明在当前PoolChunk中无法分配目标大小的内存,这一般是由于目标内存大于16M,
  // 或者当前PoolChunk已经分配了过多的内存,剩余可分配的内存不足以分配目标内存大小导致的
  if (id < 0) {
    return id;
  }

  // 更新剩余可分配内存的值
  freeBytes -= runLength(id);
  return id;
}

Esse allocateRun()método primeiro calcula o número de níveis da árvore binária correspondentes à memória de destino e, em seguida, localiza recursivamente se há um nó correspondente na árvore binária e retorna diretamente se o encontrar. Aqui continuamos a olhar para o allocateNode()método para ver como ele atravessa a árvore binária recursivamente:

private int allocateNode(int d) {
  int id = 1;
  int initial = -(1 << d);
  // 获取memoryMap中索引为id的位置的数据层数,初始时获取的就是根节点的层数
  byte val = value(id);
  // 如果更节点的层数值都比d要大,说明当前PoolChunk中没有足够的内存用于分配目标内存,直接返回-1
  if (val > d) {
    return -1;
  }

  // 这里就是通过比较当前节点的值是否比目标节点的值要小,如果要小,则说明当前节点所代表的子树是能够
  // 分配目标内存大小的,则会继续遍历其左子节点,然后遍历右子节点
  while (val < d || (id & initial) == 0) {
    id <<= 1;
    val = value(id);
    // 这里val > d其实就是表示当前节点的数值比目标数值要大,也就是说当前节点是没法申请到目标容量的内存,
    // 那么就会执行 id ^= 1,其实也就是将id切换到当前节点的兄弟节点,本质上其实就是从二叉树的
    // 左子节点开始查找,如果左子节点无法分配目标大小的内存,那么就到右子节点进行查找
    if (val > d) {
      id ^= 1;
      val = value(id);
    }
  }

  // 当找到之后,获取该节点所在的层数
  byte value = value(id);
  // 将该memoryMap中该节点位置的值设置为unusable=12,表示其已经被占用
  setValue(id, unusable);
  // 递归的更新父节点的值,使其继续保持”父节点存储的层数所代表的内存大小是未分配的
  // 子节点的层数所代表的内存之和“的语义。
  updateParentsAlloc(id);
  return id;
}

A allocateNode()lógica principal do método aqui é encontrar o valor do índice subscrito no memoryMap na memória de destino e atualizar o valor do nó pai do nó aplicado. Vamos dar uma olhada no allocateSubpage()princípio de implementação:

private long allocateSubpage(int normCapacity) {
  // 这里其实也是与PoolThreadCache中存储PoolSubpage的方式相同,也是采用分层的方式进行存储的,
  // 具体是取目标数组中哪一个元素的PoolSubpage则是根据目标容量normCapacity来进行的。
  PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity);
  int d = maxOrder;
  synchronized (head) {
    // 这里调用allocateNode()方法在二叉树中查找时,传入的d值maxOrder=11,也就是说,其本身就是
    // 直接在叶节点上查找可用的叶节点位置
    int id = allocateNode(d);
    // 小于0说明没有符合条件的内存块
    if (id < 0) {
      return id;
    }

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

    freeBytes -= pageSize;

    // 计算当前id对应的PoolSubpage数组中的位置
    int subpageIdx = subpageIdx(id);
    PoolSubpage<T> subpage = subpages[subpageIdx];
    // 这里主要是通过一个PoolSubpage对申请到的内存块进行管理,具体的管理方式我们后续文章中会进行讲解。
    if (subpage == null) {
      // 这里runOffset()方法会返回该id在PoolChunk中维护的字节数组中的偏移量位置,
      // normCapacity则记录了当前将要申请的内存大小;
      // pageSize记录了每个页的大小,默认为8KB
      subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity);
      subpages[subpageIdx] = subpage;
    } else {
      subpage.init(head, normCapacity);
    }

    // 通过PoolSubpage申请一块内存,并且返回代表该内存块的位图索引,位图索引的具体计算方式,
    // 我们前面已经简要讲述,详细的实现原理我们后面会进行讲解。
    return subpage.allocate();
  }
}

Como você pode ver aqui, o allocateSubpage()método principal é transferir a memória de 8 KB solicitada para um PoolSubpage para gerenciamento e retornar o índice de bitmap da resposta. O método de geração do parâmetro de manipulação foi explicado aqui . O princípio allocate()da initBuf()chamada do método no método é relativamente simples. Em essência, ele primeiro calcula o valor do endereço da posição inicial do bloco de memória aplicado e o comprimento do bloco de memória aplicado e, em seguida, Ele é definido como um objeto ByteBuf para inicializá-lo e seu princípio de implementação não é repetido aqui.

3.2 Liberação de memória

Em relação ao princípio de liberação de memória, é relativamente simples. Através da explicação anterior, podemos ver que a aplicação de memória é encontrar o bloco de memória que pode ser solicitado no bloco de memória principal, e então representar sua localização como o número da camada ou índice de bitmap A marca está alocada. Então, o processo de liberação aqui é realmente retornar e redefinir esses sinalizadores. Aqui explicamos o código-fonte da liberação de memória com o processo de liberação de memória direta (ByteBuffer):

void free(long handle, ByteBuffer nioBuffer) {
  int memoryMapIdx = memoryMapIdx(handle);	// 根据当前内存块在memoryMap数组中的位置
  int bitmapIdx = bitmapIdx(handle);	// 获取当前内存块的位图索引

  // 如果位图索引不等于0,说明当前内存块是小于8KB的内存块,因而将其释放过程交由PoolSubpage进行
  if (bitmapIdx != 0) {
    PoolSubpage<T> subpage = subpages[subpageIdx(memoryMapIdx)];
    PoolSubpage<T> head = arena.findSubpagePoolHead(subpage.elemSize);
    synchronized (head) {
      // 由PoolSubpage释放内存
      if (subpage.free(head, bitmapIdx & 0x3FFFFFFF)) {
        return;
      }
    }
  }

  // 走到这里说明需要释放的内存大小大于8KB,这里首先计算要释放的内存块的大小
  freeBytes += runLength(memoryMapIdx);
  // 将要释放的内存块所对应的二叉树的节点对应的值进行重置
  setValue(memoryMapIdx, depth(memoryMapIdx));
  // 将要释放的内存块所对应的二叉树的各级父节点的值进行更新
  updateParentsFree(memoryMapIdx);

  // 将创建的ByteBuf对象释放到缓存池中,以便下次申请时复用
  if (nioBuffer != null && cachedNioBuffers != null &&
      cachedNioBuffers.size() < PooledByteBufAllocator
        .DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK) {
    cachedNioBuffers.offer(nioBuffer);
  }
}

Pode-se ver que a liberação de memória aqui é principalmente para determinar se é menor que 8KB, se for menor que 8KB, será entregue ao PoolSubpage para processamento, caso contrário será zerado por uma árvore binária.

4. Resumo

Este artigo primeiro explica a estrutura geral do PoolChunk e explica em detalhes o princípio de realização da árvore binária balanceada no PoolChunk. Em seguida, cada valor de atributo em PoolChunk é descrito para ajudar os leitores a entender mais facilmente ao ler o código-fonte. Finalmente, explicamos em detalhes as duas funções principais do PoolChunk: o princípio de realização de alocação e liberação de memória.

Acho que você gosta

Origin blog.csdn.net/doubututou/article/details/109250066
Recomendado
Clasificación