Fragmentación de montón de objetos grandes

97

La aplicación C # / .NET en la que estoy trabajando sufre una pérdida de memoria lenta. He utilizado 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 haya experimentado esto antes.

La aplicación se ejecuta en el marco de 64 bits. Está calculando y serializando datos continuamente a un host remoto y está alcanzando un poco el montón de objetos grandes (LOH). Sin embargo, espero que la mayoría de los objetos LOH sean transitorios: una vez que el cálculo está completo y se ha enviado 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 de memoria libres, 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 fuera el caso si mi aplicación creara objetos grandes y duraderos durante cada cálculo. (Sí hace esto y acepto que habrá un grado de fragmentación LOH, pero ese no es el problema aquí). El problema son las matrices de objetos muy pequeñas (1056 bytes) que puede ver en el volcado anterior que no puedo ver en el código siendo creados y que permanecen arraigados de alguna manera.

También tenga en cuenta que CDB no informa el tipo cuando se descarga el segmento de montón: no estoy seguro de 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 todos cadenas y las cadenas son reconocibles a partir del código de nuestra aplicación.

Además, no puedo encontrar sus raíces GC ya que el comando! GCRoot se cuelga y nunca vuelve (incluso he intentado dejarlo durante la noche).

Por lo tanto, agradecería mucho si alguien pudiera arrojar algo de luz sobre por qué estas pequeñas matrices de objetos (<85k) terminan en el LOH: ¿en qué situaciones .NET colocará una pequeña matriz de objetos allí? Además, ¿alguien conoce alguna forma alternativa de averiguar las raíces de estos objetos?


Actualización 1

Otra teoría que se me ocurrió ayer por la noche es que estas matrices de objetos comenzaron siendo grandes pero se han reducido dejando los bloques de memoria libre que son evidentes en los volcados de memoria. Lo que me hace sospechar 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 quizás algún código inseguro en una biblioteca o en CLR esté corrompiendo el campo de número de elementos en el encabezado de la matriz. Un poco arriesgado, lo sé ...


Actualización 2

Gracias a Brian Rasmussen (ver respuesta aceptada), ¡el problema ha sido identificado como la fragmentación del LOH causado por la tabla de pasantes 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 elimina las referencias de 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 y se internan cadenas únicas. Esta acción los arraiga en la mesa de prácticas. Lo que no me di cuenta es cómo está representada la mesa de pasantes. Parece que consta de un conjunto de páginas (matrices 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)

Al realizar un volcado del segmento LOH, se 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 cuidadoso durante la pasantía. Si no se sabe que la cadena que está internando sea miembro de un conjunto finito, su aplicación se filtrará 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 utiliza los identificadores de entidad durante la desorganización: ahora sospecho firmemente que este es el culpable. Sin embargo, las intenciones del desarrollador eran obviamente buenas, ya que querían asegurarse de que si la misma entidad se deserializa varias veces, solo se mantendrá en la memoria una instancia de la cadena de identificación.

Paul Ruane
fuente
2
Gran pregunta: he notado lo mismo en mi solicitud. Pequeños objetos que quedan en el LOH después de la limpieza de los bloques grandes y causan problemas de fragmentación.
Reed Copsey
2
Estoy de acuerdo, gran pregunta. Estaré esperando respuestas.
Charlie Flowers
2
Muy interesante. ¡Parece que fue un gran problema depurarlo!
Matt Jordan

Respuestas:

46

El CLR usa el LOH para preasignar algunos objetos (como la matriz utilizada para cadenas internas ). Algunos de estos tienen menos de 85000 bytes y, por lo tanto, normalmente no se asignarían en la LOH.

Es un detalle de implementación, pero supongo que la razón de esto es evitar la recolección de basura innecesaria de instancias que se supone que sobrevivirán mientras el proceso en sí.

También debido a una optimización algo esotérica, cualquiera double[]de los 1000 o más elementos también se asigna en el LOH.

Brian Rasmussen
fuente
Los objetos problemáticos son object [] s que contienen referencias a cadenas que sé que el código de la aplicación está creando. Esto implica que la aplicación está creando los objetos [] s (no puedo ver evidencia de esto) o que alguna parte del CLR (como la serialización) los está usando para trabajar en los objetos de la aplicación.
Paul Ruane
1
Esa podría ser la estructura interna utilizada para cadenas internas. Consulte mi respuesta a esta pregunta para obtener más detalles: stackoverflow.com/questions/372547/…
Brian Rasmussen
Ah, esta es una pista muy interesante, gracias. Me olvidé por completo de la mesa de prácticas. Sé que uno de nuestros desarrolladores es un entusiasta interlocutor, por lo que definitivamente es algo que investigaré.
Paul Ruane
1
85000 bytes o 84 * 1024 = 87040 bytes?
Peter Mortensen
5
85000 bytes. Puede verificar esto creando una matriz de bytes de 85000-12 (tamaño de longitud, MT, bloque de sincronización) y llamando GC.GetGenerationa la instancia. Esto devolverá Gen2: la API no distingue entre Gen2 y LOH. Haga que la matriz sea un byte más pequeña y la API devolverá Gen0.
Brian Rasmussen
13

.NET Framework 4.5.1 tiene la capacidad de compactar explícitamente el montón de objetos grandes (LOH) durante la recolección de basura.

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

Ver más información en GCSettings.LargeObjectHeapCompactionMode

Andre Abrantes
fuente
2

Al leer las descripciones de cómo funciona GC, y la parte sobre cómo los objetos de larga vida terminan en la generación 2, y la colección de objetos LOH ocurre solo en la colección completa, al igual que la colección de la generación 2, la idea que me viene a la mente es. .. ¿por qué no mantener la generación 2 y los objetos grandes en el mismo montón, ya que se van a juntar?

Si eso es lo que realmente sucede, entonces explicaría cómo los objetos pequeños terminan en el mismo lugar que el LOH, si tienen una vida suficiente para terminar en la generación 2.

Y entonces su problema parecería ser una refutación bastante buena a la idea que se me ocurre: resultaría en la fragmentación de la LOH.

Resumen: su problema podría explicarse por el LOH y la generación 2 que comparten la misma región del montón, aunque eso no es una prueba de que esta sea la explicación.

Actualización: ¡ el resultado de !dumpheap -statprácticamente saca esta teoría del agua! La generación 2 y LOH tienen sus propias regiones.

Daniel Earwicker
fuente
Utilice! Eeheap para mostrar los segmentos que componen cada montón. Gen 0 y gen 1 viven en un segmento (el mismo segmento), gen 2 y LOH pueden asignar múltiples segmentos, pero los segmentos de cada montón permanecen separados.
Paul Ruane
Sí, vi eso, gracias. Solo quería mencionar el comando! Eeheaps ya que muestra este comportamiento de una manera mucho más clara.
Paul Ruane
La eficiencia del GC principal se debe en gran parte al hecho de que puede reubicar objetos, por lo que solo habrá una pequeña cantidad de regiones libres de memoria en el montón principal. Si un objeto del montón principal está anclado durante una recopilación, es posible que el espacio por encima y por debajo del objeto anclado deba rastrearse por separado, pero dado que la cantidad de objetos fijados es normalmente muy pequeña, también lo será el número de áreas separadas que el GC debe pista. Mezclar objetos reubicables y no reubicables (grandes) en el mismo montón perjudicaría el rendimiento.
supercat
Una pregunta más interesante es por qué .NET coloca doublematrices de más de 1000 elementos en el LOH, en lugar de ajustar el GC para asegurarse de que estén alineados en límites de 8 bytes. En realidad, incluso en un sistema de 32 bits, esperaría que, debido al comportamiento de la caché, imponer una alineación de 8 bytes en todos los objetos cuyo tamaño asignado sea un múltiplo de 8 bytes probablemente sería una ganancia de rendimiento. De lo contrario, si bien el rendimiento de uno double[]que se utiliza mucho y que está alineado con la caché sería mejor que el de uno que no lo está, no sé por qué el tamaño se correlacionaría con el uso.
supercat
@supercat Además, los dos montones también se comportan de manera muy diferente en la asignación. El montón principal es (en este momento) básicamente una pila en los patrones de asignación (siempre se asigna en la parte superior, ignorando cualquier espacio libre) cuando llega la compactación, los espacios libres se exprimen. Esto hace que la asignación sea casi imposible y ayuda a la ubicación de los datos. Por otro lado, la asignación en el LOH es similar a cómo funciona malloc: encontrará el primer lugar libre que puede contener lo que está asignando y lo asignará allí. Dado que es para objetos grandes, la ubicación de los datos es un hecho y la penalización por la asignación no es tan mala.
Luaan
1

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

El hecho de que las matrices estén intercaladas con grandes elementos liberados me lleva a suponer que originalmente estaban emparejados o al menos relacionados. Intente identificar los objetos liberados para averiguar qué los estaba generando y las cadenas asociadas.

Una vez que identifique qué está generando estas cadenas, intente averiguar qué evitaría que sean GCed. Quizás se estén metiendo 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 específico de la matriz por el momento: solo averigüe qué se está haciendo con estas cadenas para causar una fuga. Pruebe! GCRoot cuando su programa haya creado o manipulado estas cadenas solo una o dos veces, cuando haya menos objetos para rastrear.

HUAGHAGUAH
fuente
Las cadenas son una mezcla de Guids (que usamos) y claves de cadena que son fácilmente identificables. Puedo ver dónde se generan, pero nunca se agregan (directamente) a las matrices de objetos y no creamos explícitamente matrices de 128 elementos. Sin embargo, estas pequeñas matrices no deberían estar en el LOH para empezar.
Paul Ruane
1

Gran pregunta, aprendí leyendo las preguntas.

Creo que otra parte de la ruta del código de deserialización también está utilizando el montón de objetos grandes, de ahí la fragmentación. Si todas las cuerdas estuvieran internadas al MISMO momento, creo que estarías bien.

Dado lo bueno que es el recolector de basura .net, es probable que simplemente dejar que la ruta del código de deserialización cree un objeto de cadena normal sea lo suficientemente bueno. No haga nada más complejo hasta que se demuestre la necesidad.

A lo sumo, buscaría mantener una tabla hash de las últimas cadenas que ha visto y reutilizarlas. Al limitar el tamaño de la tabla hash y pasar el tamaño cuando crea la tabla, puede detener la mayor parte de la fragmentación. Luego, necesita una forma de eliminar cadenas que no ha visto recientemente de la tabla hash para limitar su tamaño. Pero si las cadenas que crea la ruta del código de deserialización son de corta duración de todos modos, no obtendrá mucho o nada.

Ian Ringrose
fuente
1

Aquí hay un par de formas de identificar la pila de llamadas exacta de LOH asignación .

Y para evitar la fragmentación de LOH, asigne previamente una gran variedad de objetos y fijelos. Reutilice estos objetos cuando sea necesario. Aquí hay una publicación sobre la fragmentación de LOH. Algo como esto podría ayudar a evitar la fragmentación de LOH.

Naveen
fuente
No veo por qué debería ayudar fijar aquí. Por cierto, los objetos grandes en LOH no son movidos por el GC de todos modos. Sin embargo, es un detalle de implementación.
user492238