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.