L'application C # / .NET sur laquelle je travaille souffre d'une lente fuite de mémoire. J'ai utilisé CDB avec SOS pour essayer de déterminer ce qui se passe mais les données ne semblent pas avoir de sens, donc j'espérais que l'un de vous en a déjà fait l'expérience.
L'application s'exécute sur le framework 64 bits. Il calcule et sérialise en permanence des données vers un hôte distant et atteint un peu le tas d'objets volumineux (LOH). Cependant, la plupart des objets LOH que je pense être transitoires: une fois le calcul terminé et envoyé à l'hôte distant, la mémoire doit être libérée. Ce que je vois, cependant, est un grand nombre de tableaux d'objets (vivants) entrelacés avec des blocs de mémoire libres, par exemple en prenant un segment aléatoire de la 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
...
Évidemment, je m'attendrais à ce que ce soit le cas si mon application créait des objets volumineux et durables pendant chaque calcul. (Il fait cela et j'accepte qu'il y aura un degré de fragmentation LOH mais ce n'est pas le problème ici.) Le problème est les très petits tableaux d'objets (1056 octets) que vous pouvez voir dans le vidage ci-dessus que je ne peux pas voir dans le code en cours de création et qui restent enracinés d'une manière ou d'une autre.
Notez également que CDB ne signale pas le type lorsque le segment de tas est vidé: je ne sais pas si cela est lié ou non. Si je vide l'objet marqué (<-), CDB / SOS le signale 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
Les éléments du tableau d'objets sont tous des chaînes et les chaînes sont reconnaissables à partir de notre code d'application.
De plus, je suis incapable de trouver leurs racines GC car la commande! GCRoot se bloque et ne revient jamais (j'ai même essayé de la laisser du jour au lendemain).
Donc, j'apprécierais beaucoup si quelqu'un pouvait expliquer pourquoi ces petits tableaux d'objets (<85k) se retrouvent sur le LOH: dans quelles situations .NET mettra-t-il un petit tableau d'objets? De plus, est-ce que quelqu'un connaît une autre façon de déterminer les racines de ces objets?
Mise à jour 1
Une autre théorie que j'ai proposée tard hier est que ces tableaux d'objets ont commencé en grand mais ont été réduits en laissant les blocs de mémoire libre qui sont évidents dans les vidages de mémoire. Ce qui me rend suspect, c'est que les tableaux d'objets semblent toujours avoir une longueur de 1056 octets (128 éléments), 128 * 8 pour les références et 32 octets de surcharge.
L'idée est que peut-être un code non sécurisé dans une bibliothèque ou dans le CLR endommage le champ du nombre d'éléments dans l'en-tête du tableau. Un peu long, je sais ...
Mise à jour 2
Grâce à Brian Rasmussen (voir la réponse acceptée), le problème a été identifié comme une fragmentation de la LOH causée par la table des chaînes internes! J'ai écrit une application de test rapide pour confirmer ceci:
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();
}
L'application crée et déréférence d'abord des chaînes uniques dans une boucle. C'est juste pour prouver que la mémoire ne fuit pas dans ce scénario. De toute évidence, il ne devrait pas et ce ne l'est pas.
Dans la deuxième boucle, des chaînes uniques sont créées et internées. Cette action les enracine dans la table interne. Ce que je ne savais pas, c'est comment la table interne est représentée. Il semble qu'il se compose d'un ensemble de pages - des tableaux d'objets de 128 éléments de chaîne - qui sont créés dans le LOH. Ceci est plus évident dans 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)
Faire un vidage du segment LOH révèle le modèle que j'ai vu dans l'application qui fuit:
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
Notez que la taille du tableau d'objets est de 528 (au lieu de 1056) car mon poste de travail est de 32 bits et le serveur d'applications est de 64 bits. Les tableaux d'objets comportent toujours 128 éléments.
Donc, la morale de cette histoire est d'être un stage très prudent. Si la chaîne que vous effectuez n'est pas connue pour être membre d'un ensemble fini, votre application fuira en raison de la fragmentation du LOH, au moins dans la version 2 du CLR.
Dans le cas de notre application, il y a du code général dans le chemin du code de désérialisation qui intègre les identifiants d'entité lors de la démarshalling: je soupçonne maintenant fortement que c'est le coupable. Cependant, les intentions du développeur étaient évidemment bonnes car ils voulaient s'assurer que si la même entité est désérialisée plusieurs fois, une seule instance de la chaîne d'identifiant sera conservée en mémoire.