[C#] Fragmentación de montón de objetos grandes



Answers

.NET Framework 4.5.1 tiene la capacidad de compactar de forma explícita el gran montón de objetos (LOH) durante la recolección de elementos no utilizados.

GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();

Ver más información en GCSettings.LargeObjectHeapCompactionMode

Question

La aplicación C # / .NET en la que estoy trabajando sufre una lenta pérdida de memoria. He usado CDB con SOS para tratar de determinar qué está sucediendo, pero los datos no parecen tener ningún sentido, así que esperaba que alguno de ustedes ya haya experimentado esto antes.

La aplicación se ejecuta en el marco de 64 bits. Está continuamente calculando y serializando datos a un host remoto y está golpeando el Gran Montón de Objetos (LOH) un poco. Sin embargo, la mayoría de los objetos LOH espero que sean transitorios: una vez que se completa el cálculo y se envía al host remoto, la memoria debe liberarse. Lo que estoy viendo, sin embargo, es una gran cantidad de matrices de objetos (en vivo) intercaladas con bloques libres de memoria, por ejemplo, tomando un segmento aleatorio del LOH:

0:000> !DumpHeap 000000005b5b1000  000000006351da10
         Address               MT     Size
...
000000005d4f92e0 0000064280c7c970 16147872
000000005e45f880 00000000001661d0  1901752 Free
000000005e62fd38 00000642788d8ba8     1056       <--
000000005e630158 00000000001661d0  5988848 Free
000000005ebe6348 00000642788d8ba8     1056
000000005ebe6768 00000000001661d0  6481336 Free
000000005f214d20 00000642788d8ba8     1056
000000005f215140 00000000001661d0  7346016 Free
000000005f9168a0 00000642788d8ba8     1056
000000005f916cc0 00000000001661d0  7611648 Free
00000000600591c0 00000642788d8ba8     1056
00000000600595e0 00000000001661d0   264808 Free
...

Obviamente, esperaría que este sea el caso si mi aplicación estuviera creando objetos largos y de larga vida durante cada cálculo. (Hace esto y acepto que habrá un cierto grado de fragmentación de LOH, pero ese no es el problema aquí). El problema son los arreglos de objetos muy pequeños (1056 bytes) que puedes ver en el volcado anterior que no puedo ver en el código siendo creados y que permanecen enraizados de alguna manera.

También tenga en cuenta que CDB no informa el tipo cuando el segmento de montón se vierte: no estoy seguro si esto está relacionado o no. Si vuelco el objeto marcado (<-), CDB / SOS lo informa bien:

0:015> !DumpObj 000000005e62fd38
Name: System.Object[]
MethodTable: 00000642788d8ba8
EEClass: 00000642789d7660
Size: 1056(0x420) bytes
Array: Rank 1, Number of elements 128, Type CLASS
Element Type: System.Object
Fields:
None

Los elementos de la matriz de objetos son todas cadenas y las cadenas son reconocibles desde nuestro código de aplicación.

Además, no puedo encontrar sus raíces de GC ya que el comando! GCRoot se bloquea y nunca vuelve (incluso he intentado dejarlo de la noche a la mañana).

Por lo tanto, agradecería mucho si alguien pudiera arrojar alguna luz sobre por qué estos pequeños (<85k) conjuntos de objetos están terminando en el LOH: ¿qué situaciones va a poner .NET una pequeña matriz de objetos allí? Además, ¿alguien sabe de una forma alternativa de determinar las raíces de estos objetos?

Actualización 1

Otra teoría que surgió ayer tarde es que estas matrices de objetos comenzaron siendo grandes pero se redujeron dejando los bloques de memoria libre que son evidentes en los volcados de memoria. Lo que me hace sospechoso es que las matrices de objetos siempre parecen tener 1056 bytes de longitud (128 elementos), 128 * 8 para las referencias y 32 bytes de sobrecarga.

La idea es que tal vez algún código inseguro en una biblioteca o en el CLR esté corrompiendo el número de elementos en el encabezado de la matriz. Un poco de una posibilidad remota, lo sé ...

Actualización 2

Gracias a Brian Rasmussen (ver respuesta aceptada), el problema ha sido identificado como la fragmentación de LOH causada por la tabla de internos de cuerdas. Escribí una aplicación de prueba rápida para confirmar esto:

static void Main()
{
    const int ITERATIONS = 100000;

    for (int index = 0; index < ITERATIONS; ++index)
    {
        string str = "NonInterned" + index;
        Console.Out.WriteLine(str);
    }

    Console.Out.WriteLine("Continue.");
    Console.In.ReadLine();

    for (int index = 0; index < ITERATIONS; ++index)
    {
        string str = string.Intern("Interned" + index);
        Console.Out.WriteLine(str);
    }

    Console.Out.WriteLine("Continue?");
    Console.In.ReadLine();
}

La aplicación primero crea y desreferencia cadenas únicas en un bucle. Esto es solo para demostrar que la memoria no se filtra en este escenario. Obviamente no debería y no es así.

En el segundo ciclo, se crean e internan cadenas únicas. Esta acción los enraiza en la mesa de pasante. Lo que no me di cuenta es cómo se representa la mesa interna. Parece que consiste en un conjunto de páginas (conjuntos de objetos de 128 elementos de cadena) que se crean en el LOH. Esto es más evidente en CDB / SOS:

0:000> .loadby sos mscorwks
0:000> !EEHeap -gc
Number of GC Heaps: 1
generation 0 starts at 0x00f7a9b0
generation 1 starts at 0x00e79c3c
generation 2 starts at 0x00b21000
ephemeral segment allocation context: none
 segment    begin allocated     size
00b20000 00b21000  010029bc 0x004e19bc(5118396)
Large object heap starts at 0x01b21000
 segment    begin allocated     size
01b20000 01b21000  01b8ade0 0x00069de0(433632)
Total Size  0x54b79c(5552028)
------------------------------
GC Heap Size  0x54b79c(5552028)

Tomar un vertedero del segmento LOH revela el patrón que vi en la aplicación con fugas:

0:000> !DumpHeap 01b21000 01b8ade0
...
01b8a120 793040bc      528
01b8a330 00175e88       16 Free
01b8a340 793040bc      528
01b8a550 00175e88       16 Free
01b8a560 793040bc      528
01b8a770 00175e88       16 Free
01b8a780 793040bc      528
01b8a990 00175e88       16 Free
01b8a9a0 793040bc      528
01b8abb0 00175e88       16 Free
01b8abc0 793040bc      528
01b8add0 00175e88       16 Free    total 1568 objects
Statistics:
      MT    Count    TotalSize Class Name
00175e88      784        12544      Free
793040bc      784       421088 System.Object[]
Total 1568 objects

Tenga en cuenta que el tamaño de la matriz de objetos es 528 (en lugar de 1056) porque mi estación de trabajo es de 32 bits y el servidor de aplicaciones es de 64 bits. Las matrices de objetos todavía tienen 128 elementos de longitud.

Entonces, la moraleja de esta historia es ser muy cuidadosos en las prácticas. Si no se sabe que la cadena que está interviniendo es miembro de un conjunto finito, su aplicación se fugará debido a la fragmentación del LOH, al menos en la versión 2 del CLR.

En el caso de nuestra aplicación, hay un código general en la ruta del código de deserialización que interna a los identificadores de la entidad durante el desasignamiento: ahora sospecho fuertemente que este es el culpable. Sin embargo, las intenciones del desarrollador fueron obviamente buenas, ya que querían asegurarse de que si la misma entidad se deserializaba varias veces, solo se mantendría en la memoria una sola instancia de la cadena identificadora.




Si el formato es reconocible como su aplicación, ¿por qué no ha identificado el código que está generando este formato de cadena? Si hay varias posibilidades, intente agregar datos únicos para descubrir qué ruta de código es la culpable.

El hecho de que las matrices estén intercaladas con elementos liberados grandes me lleva a adivinar que originalmente estaban emparejados o al menos relacionados. Intente identificar los objetos liberados para descubrir qué fue lo que los generó y las cadenas asociadas.

Una vez que identifica lo que está generando estas cadenas, intente descubrir qué evitaría que se convirtieran en GC. Tal vez están siendo rellenados en una lista olvidada o no utilizada para fines de registro o algo similar.

EDITAR: Ignore la región de memoria y el tamaño de matriz específico para el momento: simplemente descubra qué se está haciendo con estas cadenas para provocar una fuga. Pruebe el! GCRoot cuando su programa haya creado o manipulado estas cadenas una o dos veces, cuando haya menos objetos para rastrear.




Aquí hay algunas maneras de identificar la call-stack de call-stack exacta de la asignación de LOH .

Y para evitar la fragmentación de LOH, asigne previamente una gran variedad de objetos y péguelos. Reutilice estos objetos cuando sea necesario. Aquí está la post de LOH Fragmentation. Algo como esto podría ayudar a evitar la fragmentación de LOH.






Links