web-dev-qa-db-fra.com

Fragmentation de tas d'objets volumineux

L'application C # /. NET sur laquelle je travaille souffre d'une fuite de mémoire lente. 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 pourrait avoir déjà vécu cela.

L'application s'exécute sur le framework 64 bits. Il calcule et sérialise en continu des données vers un hôte distant et touche assez bien le tas d'objets volumineux (LOH). Cependant, la plupart des objets LOH que je prévois sont 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, c'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 de grands objets à longue durée de vie lors de chaque calcul. (Il le fait et j'accepte qu'il y ait un certain 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 en quelque sorte.

Notez également que CDB ne signale pas le type lorsque le segment de segment est vidé: je ne sais pas si cela est lié ou non. Si je vide l'objet marqué (<-), CDB/SOS le signale correctement:

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 toutes des chaînes et les chaînes sont reconnaissables à partir de notre code d'application.

De plus, je ne peux pas 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 que quelqu'un puisse faire la lumière sur la raison pour laquelle ces petits tableaux d'objets (<85k) se retrouvent sur la LOH: quelles situations .NET mettra-t-il un petit tableau d'objets là-dedans? Par ailleurs, quelqu'un connaît-il une autre façon de déterminer les racines de ces objets?


Mise à jour 1

Une autre théorie que j'ai trouvée tard hier est que ces tableaux d'objets ont commencé à grande échelle mais ont été réduits 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 du code non sûr dans une bibliothèque ou dans le CLR altère le champ du nombre d'éléments dans l'en-tête du tableau. Un peu loin, 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 interne des chaînes! J'ai écrit une application de test rapide pour confirmer cela:

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. Évidemment, cela ne devrait pas et ce n'est pas le cas.

Dans la deuxième boucle, des chaînes uniques sont créées et internées. Cette action les enracine dans la table des stagiaires. Ce que je ne savais pas, c'est comment la table des stagiaires est représentée. Il semble qu'il se compose d'un ensemble de pages - tableaux d'objets de 128 éléments de chaîne - qui sont créés dans la 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 motif 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 528 (plutôt que 1056) car ma station de travail est 32 bits et le serveur d'applications est 64 bits. Les tableaux d'objets font toujours 128 éléments.

La morale de cette histoire est donc d'être très prudent en internat. Si la chaîne que vous internez n'est pas connue pour être membre d'un ensemble fini, votre application fuira en raison de la fragmentation de la 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 internifie les identifiants d'entité lors du démasquage: 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é était désérialisée plusieurs fois, alors une seule instance de la chaîne d'identifiants serait maintenue en mémoire.

96
Paul Ruane

Le CLR utilise la LOH pour préallouer quelques objets (tels que le tableau utilisé pour les chaînes internes ). Certains d'entre eux sont inférieurs à 85 000 octets et ne seraient donc normalement pas alloués sur la LOH.

Il s'agit d'un détail d'implémentation, mais je suppose que la raison en est d'éviter le ramasse-miettes inutile des instances qui sont censées survivre aussi longtemps que le processus lui-même.

En raison également d'une optimisation quelque peu ésotérique, tout double[] de 1000 éléments ou plus est également alloué sur la LOH.

45
Brian Rasmussen

Le .NET Framework 4.5.1, a la capacité de compacter explicitement le tas d'objets volumineux (LOH) pendant le garbage collection.

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

Voir plus d'informations dans GCSettings.LargeObjectHeapCompactionMode

12
Andre Abrantes

Lors de la lecture des descriptions du fonctionnement de GC et de la partie sur la durée de vie des objets dans la génération 2, et la collecte des objets LOH se produit uniquement lors de la collecte complète - tout comme la collecte de la génération 2, l'idée qui vient à l'esprit est. .. pourquoi ne pas simplement garder la génération 2 et les gros objets dans le même tas, car ils vont être collectés ensemble?

Si c'est ce qui se passe réellement, cela expliquerait comment les petits objets se retrouvent au même endroit que le LOH - s'ils ont une durée de vie suffisante pour se retrouver dans la génération 2.

Et donc votre problème semble être une assez bonne réfutation de l'idée qui me vient à l'esprit - cela entraînerait la fragmentation de la LOH.

Résumé: votre problème pourrait être expliqué par la LOH et la génération 2 partageant la même région de tas, bien que cela ne prouve nullement que c'est l'explication.

pdate: la sortie de !dumpheap -stat jette à peu près cette théorie hors de l'eau! La génération 2 et LOH ont leurs propres régions.

2
Daniel Earwicker

Voici quelques façons d'identifier l'allocation exacte call-stack of LOH .

Et pour éviter la fragmentation LOH Pré-allouez un large éventail d'objets et épinglez-les. Réutilisez ces objets si nécessaire. Voici post sur la fragmentation LOH. Quelque chose comme ça pourrait aider à éviter la fragmentation de LOH.

1
Naveen

Si le format est reconnaissable comme votre application, pourquoi n'avez-vous pas identifié le code qui génère ce format de chaîne? S'il existe plusieurs possibilités, essayez d'ajouter des données uniques pour déterminer quel chemin de code est le coupable.

Le fait que les tableaux soient entrelacés avec de gros éléments libérés me fait supposer qu'ils étaient à l'origine associés ou du moins liés. Essayez d'identifier les objets libérés pour comprendre ce qui les a générés et les chaînes associées.

Une fois que vous avez identifié ce qui génère ces chaînes, essayez de comprendre ce qui les empêcherait d'être GCed. Peut-être qu'ils sont placés dans une liste oubliée ou inutilisée à des fins de journalisation ou quelque chose de similaire.


EDIT: Ignorez la région de la mémoire et la taille spécifique du tableau pour le moment: déterminez simplement ce qui est fait avec ces chaînes pour provoquer une fuite. Essayez le! GCRoot lorsque votre programme a créé ou manipulé ces chaînes une ou deux fois, lorsqu'il y a moins d'objets à tracer.

1
HUAGHAGUAH

Grande question, j'ai appris en lisant les questions.

Je pense qu'un autre morceau du chemin du code de désérialisation utilise également le tas d'objets volumineux, d'où la fragmentation. Si toutes les cordes étaient internées au MÊME moment, je pense que ça irait.

Étant donné la qualité du ramasse-miettes .net, le simple fait de laisser le chemin du code de désérialisation créer un objet chaîne normal est probablement suffisant. Ne faites rien de plus complexe jusqu'à ce que le besoin soit prouvé.

Je voudrais tout au plus envisager de conserver une table de hachage des dernières chaînes que vous avez vues et de les réutiliser. En limitant la taille de la table de hachage et en transmettant la taille lorsque vous créez la table, vous pouvez arrêter la plupart de la fragmentation. Vous avez ensuite besoin d'un moyen de supprimer les chaînes que vous n'avez pas vues récemment de la table de hachage pour limiter sa taille. Mais si les chaînes que le chemin du code de désérialisation crée sont de courte durée de toute façon, vous ne gagnerez pas grand-chose, sinon rien.

1
Ian Ringrose
1
Jose Gzz