c# .net memory-management memory-leaks windbg

c# - Fragmentación de montón de objetos grandes



.net memory-management (7)

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.


.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


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

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

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

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

Actualización: ¡ la salida de !dumpheap -stat prácticamente elimina esta teoría del agua! La generación 2 y LOH tienen sus propias regiones.


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.


CLR usa 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án al LOH.

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

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



Gran pregunta, lo aprendí leyendo las preguntas.

Creo que otra parte de la ruta del código de deserialización también está utilizando el gran montón de objetos, de ahí la fragmentación. Si todas las cadenas se internan al MISMO momento, creo que estarás bien.

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

A lo sumo trataré de mantener una tabla hash de las últimas cadenas que haya 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ía de la fragmentación. Luego necesita una forma de eliminar cadenas que no ha visto recientemente en 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 ganarás mucho, si acaso.


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.