Grupo de memoria de Netty: una explicación detallada del principio de PoolChunk, ver aquí ~

PoolChunkNetty es una parte importante del pool de memoria, su función principal es mantener un gran bloque de memoria, cuando sea necesario aplicar más de 8KB de memoria, será a partir de la PoolChunkadquisición de. Este artículo primero PoolChunkexplicará la estructura general, luego explicará el papel de sus atributos principales y finalmente PoolChunkcómo realizar la aplicación y liberación de grandes bloques de memoria desde la perspectiva del código fuente .

1. Estructura general de PoolChunk

PoolChunkEl tamaño de memoria predeterminado solicitado es 16M. En términos de estructura, organizará la memoria de 16M en un árbol binario balanceado. El tamaño de memoria representado por cada nodo de cada capa del árbol binario es igual, y cada capa del nodo representa La suma del tamaño total de la memoria es 16M, el número total de capas de todo el árbol binario es 12 y el número de capas comienza desde 0. El diagrama de estructura es el siguiente:

Con respecto a la imagen de arriba, principalmente necesitamos explicar los siguientes puntos:

  • La memoria total ocupada por un PoolChunk es 16M. Dividirá la memoria de 16M según el nivel actual del árbol binario. Por ejemplo, la primera capa la dividirá en dos 8M, la segunda capa la dividirá en cuatro 4M, etc. El árbol binario tiene 12 niveles como máximo, por lo que el tamaño de memoria de cada nodo hoja es de 8KB, es decir, la memoria que puede asignar PoolChunk es de al menos 8KB y como máximo de 16M;
  • Se puede ver que hay 2 ^ 11 = 2048 nodos hoja en el árbol binario de la figura, por lo que el número de nodos en todo el árbol es 4095. PoolChunk colocó en mosaico estos 4095 nodos en una matriz con una longitud de 4096. El primer bit almacena 0, el segundo y tercer bits almacenan 1, y el cuarto y séptimo bits almacenan 2, y así sucesivamente, el conjunto es en realidad Es decir, el árbol binario representado por el número de capa se almacena en una matriz. La matriz aquí es el mapa de profundidad de la izquierda. A través de este árbol binario, el número de capas se puede obtener rápidamente a través del subíndice. Por ejemplo, el valor de la posición 2048 es 11, lo que significa que está en la 11ª capa del árbol binario. La estructura de depthMap se muestra a continuación:

  • En cada nodo del árbol binario de la figura, marcamos un número para el tamaño de memoria representado por el nodo actual. Este número realmente representa el tamaño de memoria que el nodo actual puede asignar, como 0 para 16M, 1 para 8M, etc. . Estos números son almacenados por memoryMap, que representa el tamaño de la memoria asignable representada por cada nodo en el árbol binario, y su estructura de datos es exactamente la misma que la de depthMap. En la figura, el tamaño de la memoria asignable representada por cada nodo principal es igual a la suma de los dos nodos secundarios. Si se ha asignado la memoria de un nodo secundario, el nodo se marcará como 12, lo que indica que se ha asignado y su El nodo padre se actualizará al valor de otro nodo hijo, lo que indica que la memoria que el nodo padre puede asignar es la suma de la memoria proporcionada por sus dos nodos hijos;
  • Para el proceso general de aplicación y liberación de memoria de PoolChunk, describiremos la aplicación para 9 KB de memoria:
    1. Primero, 9KB = 9126 se expande a un exponente mayor que los primeros 2, es decir, 2 << 13 = 16384. Dado que el nodo hoja 8KB = 2 << 12, el número correspondiente de capas es 11, por lo que el número de capas donde se ubica 16384 Es 10, lo que significa que puede encontrar un nodo no asignado de la décima capa del grupo de memoria;
    2. Una vez que el nodo de destino está en la décima capa, la comparación comenzará desde el nodo principal. Si el valor almacenado por el nodo principal es menor que 10, significa que tiene suficiente memoria para asignar la memoria de destino, y luego El nodo hijo izquierdo se compara con 10, si el nodo hijo izquierdo es mayor que 10 (generalmente el nodo hijo izquierdo se ha asignado en este momento, y su valor es 12, por lo que será mayor que 10), entonces el nodo hijo derecho será 10 a modo de comparación, en este momento el nodo hijo derecho es definitivamente más pequeño que 10, entonces la comparación anterior continuará desde el nodo hijo izquierdo del nodo hijo derecho;
    3. Al comparar con un momento determinado, el número de un nodo es igual a 10, significa que esta ubicación es el bloque de memoria que necesitamos, luego se marcará como 12, y luego retrocederá recursivamente, la memoria representada por su nodo padre El valor se actualiza al valor de otro nodo hijo;

  • Con respecto a la asignación de memoria, el último problema que debe explicarse aquí es que a través del método de cálculo anterior, podemos encontrar un nodo como nuestro nodo de destino para ser asignado. En este momento, debemos devolver la dirección de inicio y la longitud de la memoria representada por este nodo. . Dado que solo tenemos el valor de la dirección de la memoria de 16M solicitada por todo el PoolChunk, el desplazamiento del nodo en relación con la dirección inicial de todo el bloque de memoria se puede calcular a través del número de capa del nodo de destino y el número de nodos en la capa. De esta manera, se puede obtener el valor de la dirección inicial del nodo; acerca de la longitud de memoria ocupada por el nodo, la sensación intuitiva puede entenderse como un mapeo, como 11 para una longitud de 8KB, 10 para una longitud de 16KB y así sucesivamente. Por supuesto, el cálculo de la dirección de inicio y el desplazamiento aquí no lo implementa directamente PoolChunk a través de este algoritmo, sino a través de operaciones de bits más eficientes.

2. El papel de los principales atributos de PoolChunk

Al leer el código fuente de la reserva de memoria de Netty, creo que la mayoría de los lectores se sentirán confundidos por sus diversos atributos complejos, lo que dificulta su comprensión. Aquí enumeramos sus atributos por separado para facilitar a los lectores a comprender el papel de cada atributo más rápido al leer el código fuente.

// 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. Implementación del código fuente

En cuanto a PoolChunklas funciones, explicamos principalmente el proceso de asignación y recuperación de memoria.

3.1 Asignación de memoria

PoolChunkLa asignación de memoria de está principalmente en su allocate()método, y la descripción general de la asignación se ha explicado anteriormente, por lo que no la repetiré aquí, ingresaremos directamente su código fuente para leer:

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;
}

Se puede ver que para la asignación de memoria, se juzga principalmente si es mayor de 8KB. Si es mayor de 8KB, se asignará directamente en el árbol binario de PoolChunk. Si es menor de 8KB, se aplicará directamente a una memoria de 8KB y luego a 8KB de memoria. Entregado a una subpágina de la piscina para su mantenimiento. En cuanto al principio de implementación de PoolSubpage, lo explicaremos más adelante, aquí solo lo explicamos brevemente para ayudar a los lectores a comprender el concepto de índice de mapa de bits. Cuando solicitamos 8 KB de memoria del árbol binario de PoolChunk, se entregará a una PoolSubpage para su mantenimiento. En PoolSubpage, dividirá todo el tamaño del bloque de memoria en una serie de 16 bytes, aquí es 8KB, es decir, se dividirá en 512 = 8KB / 16byte. Para identificar si cada copia está ocupada, PoolSubpage usa una matriz larga para representarla. El nombre de la matriz es mapa de bits, por lo que lo llamamos matriz de mapa de bits. Para indicar si 512 piezas de datos están ocupadas o no, un long tiene solo 64 bytes, por lo que se necesitan 8 = 512/64 longs aquí para representar, por lo que la matriz long se usa aquí en lugar de un solo campo largo. Pero el lector debería haber descubierto que los 32 bits altos del identificador aquí son un valor entero, y el mapa de bits que describimos es una matriz larga, entonces, ¿cómo indica un número entero que la memoria solicitada actualmente es el elemento numérico en la matriz de mapa de bits? Y el número del entero 64 bytes en el elemento. En realidad, esto está representado por los 67 bits bajos del entero. Los bits 64 y 67 se utilizan para indicar el número de elementos largos en la matriz de mapa de bits actualmente ocupada, y los 1 64 bits se utilizan para indicar el número de elementos largos en el elemento largo. Cuántos están ocupados por la aplicación actual. Por lo tanto, solo se necesita un identificador de entero largo para indicar la posición de la memoria aplicada actualmente en todo el grupo de memoria y la posición en PoolSubpage. Aquí primero leemos allocateRun()el código fuente del 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;
}

Este allocateRun()método primero calcula el número de niveles de árbol binario correspondientes a la memoria de destino, y luego encuentra de forma recursiva si hay un nodo correspondiente en el árbol binario y regresa directamente si lo encuentra. Aquí continuamos mirando el allocateNode()método para ver cómo atraviesa el árbol binario de forma recursiva:

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;
}

La allocateNode()lógica principal del método aquí es encontrar el valor del subíndice del índice en el memoryMap en la memoria de destino y actualizar el valor del nodo principal del nodo aplicado. Echemos un vistazo al allocateSubpage()principio de implementación:

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 puede ver aquí, el allocateSubpage()método principal es transferir la memoria de 8 KB solicitada a una PoolSubpage para su administración y devolver el índice de mapa de bits de la respuesta. El método de generación del parámetro de control se ha explicado aquí . El principio allocate()de la initBuf()llamada al método en el método es relativamente simple. En esencia, primero calcula el valor de dirección de la posición inicial del bloque de memoria aplicado y la longitud del bloque de memoria aplicado, y luego Se establece en un objeto ByteBuf para inicializarlo y su principio de implementación no se repite aquí.

3.2 Liberación de memoria

En cuanto al principio de liberación de memoria, es relativamente simple. A través de la explicación anterior, podemos ver que la aplicación de la memoria es encontrar el bloque de memoria que se puede solicitar en el bloque de memoria principal, y luego representar su ubicación como el número de capa o el índice de mapa de bits. Se asigna la marca. Entonces, el proceso de liberación aquí es en realidad regresar y luego restablecer estas banderas. Aquí explicamos el código fuente de la liberación de memoria con el proceso de liberación de memoria directa (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);
  }
}

Se puede ver que la liberación de la memoria aquí es principalmente para determinar si es menor a 8KB, si es menor a 8KB, se entregará a PoolSubpage para su procesamiento, de lo contrario, se restablecerá mediante un árbol binario.

4. Resumen

Este artículo primero explica la estructura general de PoolChunk y explica en detalle el principio de realización del árbol binario balanceado en PoolChunk. Luego, se describe cada valor de atributo en PoolChunk para ayudar a los lectores a comprender más fácilmente al leer el código fuente. Finalmente, explicamos en detalle las dos funciones principales de PoolChunk: el principio de realización de asignación y liberación de memoria.

Supongo que te gusta

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