it-swarm.com.de

Große Objekthaufenfragmentierung

Die C # /. NET-Anwendung, an der ich arbeite, weist einen langsamen Speicherverlust auf. Ich habe CDB mit SOS verwendet, um festzustellen, was passiert, aber die Daten scheinen keinen Sinn zu ergeben. Ich hatte gehofft, dass einer von Ihnen dies schon einmal erlebt hat.

Die Anwendung wird auf dem 64-Bit-Framework ausgeführt. Es berechnet und serialisiert kontinuierlich Daten für einen Remote-Host und trifft den Large Object Heap (LOH) ein gutes Stück. Die meisten LOH-Objekte werden jedoch voraussichtlich vorübergehend sein: Sobald die Berechnung abgeschlossen und an den Remote-Host gesendet wurde, sollte der Speicher freigegeben werden. Was ich jedoch sehe, ist eine große Anzahl von (lebenden) Objektarrays, die mit freien Speicherblöcken verschachtelt sind, z. B. indem ein zufälliges Segment aus dem LOH entnommen wird:

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
...

Natürlich würde ich dies erwarten, wenn meine Anwendung bei jeder Berechnung langlebige, große Objekte erzeugt. (Es tut dies und ich akzeptiere, dass es einen Grad an LOH-Fragmentierung geben wird, aber das ist hier nicht das Problem.) Das Problem sind die sehr kleinen (1056 Byte) Objektarrays, die Sie im obigen Dump sehen können, die ich im Code nicht sehen kann geschaffen werden und die irgendwie verwurzelt bleiben.

Beachten Sie auch, dass CDB den Typ nicht meldet, wenn das Heap-Segment gesichert wird: Ich bin nicht sicher, ob dies zusammenhängt oder nicht. Wenn ich das markierte (<-) Objekt ablege, meldet CDB/SOS dies in Ordnung:

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

Die Elemente des Objektarrays sind alle Zeichenfolgen, und die Zeichenfolgen sind aus unserem Anwendungscode erkennbar.

Außerdem kann ich ihre GC-Wurzeln nicht finden, da der Befehl! GCRoot hängt und nie zurückkommt (ich habe sogar versucht, ihn über Nacht zu lassen).

Ich würde es sehr begrüßen, wenn jemand Aufschluss darüber geben könnte, warum diese kleinen (<85k) Objektarrays auf dem LOH landen: In welchen Situationen wird .NET dort ein kleines Objektarray platzieren? Kennt jemand zufällig eine alternative Methode zur Ermittlung der Wurzeln dieser Objekte?


Update 1

Eine andere Theorie, die ich gestern spät aufgestellt habe, ist, dass diese Objekt-Arrays groß angefangen haben, aber geschrumpft sind und die in den Speicherabbildern sichtbaren freien Speicherblöcke übrig geblieben sind. Was mich misstrauisch macht, ist, dass die Objektarrays immer 1056 Byte lang zu sein scheinen (128 Elemente), 128 * 8 für die Referenzen und 32 Byte Overhead.

Die Idee ist, dass möglicherweise ein unsicherer Code in einer Bibliothek oder in der CLR das Feld für die Anzahl der Elemente im Array-Header beschädigt. Ich weiß, dass das ein langer Weg ist ...


Update 2

Dank Brian Rasmussen (siehe akzeptierte Antwort) wurde das Problem als Fragmentierung des LOH durch die String-Intern-Tabelle identifiziert! Ich habe eine schnelle Testanwendung geschrieben, um dies zu bestätigen:

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();
}

Die Anwendung erstellt und dereferenziert zunächst eindeutige Zeichenfolgen in einer Schleife. Dies soll nur beweisen, dass der Speicher in diesem Szenario nicht leckt. Offensichtlich sollte es nicht und tut es nicht.

In der zweiten Schleife werden eindeutige Zeichenfolgen erstellt und interniert. Diese Aktion löscht sie in der internen Tabelle. Was ich nicht mitbekommen habe, ist, wie der interne Tisch dargestellt wird. Es scheint, dass es aus einer Reihe von Seiten besteht - Objektarrays mit 128 Zeichenfolgenelementen -, die im LOH erstellt werden. Dies ist deutlicher in 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)

Wenn Sie einen Dump des LOH-Segments machen, sehen Sie das Muster, das ich in der undichten Anwendung gesehen habe:

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

Beachten Sie, dass die Größe des Objektarrays 528 (statt 1056) beträgt, da meine Arbeitsstation 32 Bit und der Anwendungsserver 64 Bit hat. Die Objektarrays sind immer noch 128 Elemente lang.

Die Moral dieser Geschichte ist es also, sehr vorsichtig mit dem Internieren umzugehen. Wenn die Zeichenfolge, die Sie internieren, nicht als Mitglied einer endlichen Menge bekannt ist, läuft Ihre Anwendung aufgrund der Fragmentierung des LOH aus, zumindest in Version 2 der CLR.

Im Fall unserer Anwendung gibt es einen allgemeinen Code im Deserialisierungs-Codepfad, der die Entitäts-IDs während des Unmarshalling interniert: Ich vermute, dass dies der Schuldige ist. Die Absichten des Entwicklers waren jedoch offensichtlich gut, da er sicherstellen wollte, dass nur eine Instanz der Bezeichnerzeichenfolge im Speicher erhalten bleibt, wenn dieselbe Entität mehrmals deserialisiert wird.

96
Paul Ruane

Die CLR verwendet das LOH, um einige Objekte vorab zuzuweisen (z. B. das Array, das für interne Zeichenfolgen verwendet wird ). Einige davon sind kleiner als 85000 Bytes und werden daher normalerweise nicht auf dem LOH zugewiesen.

Es ist ein Implementierungsdetail, aber ich gehe davon aus, dass der Grund dafür darin besteht, unnötige Speicherbereinigungen von Instanzen zu vermeiden, die so lange überleben sollen wie der Prozess selbst.

Auch aufgrund einer etwas esoterischen Optimierung kann jeder double[] von 1000 oder mehr Elementen wird auch auf dem LOH zugewiesen.

45
Brian Rasmussen

.NET Framework 4.5.1 bietet die Möglichkeit, den Large Object Heap (LOH) während der Garbage Collection explizit zu komprimieren.

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

Weitere Informationen finden Sie unter GCSettings.LargeObjectHeapCompactionMode

12
Andre Abrantes

Beim Lesen von Beschreibungen der Funktionsweise von GC und des Teils darüber, wie langlebige Objekte zur zweiten Generation gehören, und beim Sammeln von LOH-Objekten nur bei vollständiger Sammlung - genau wie bei der Sammlung der zweiten Generation - fällt die Idee ein. .. warum nicht Generation 2 und große Objekte auf dem gleichen Haufen halten, da sie zusammen gesammelt werden?

Wenn dies tatsächlich der Fall ist, würde dies erklären, wie kleine Objekte am selben Ort wie der LOH enden - wenn sie lange genug leben, um in Generation 2 zu enden.

Und so scheint Ihr Problem eine ziemlich gute Widerlegung der Idee zu sein, die mir einfällt - es würde zur Fragmentierung des LOH führen.

Zusammenfassung: Ihr Problem könnte dadurch erklärt werden, dass sich LOH und Generation 2 dieselbe Heap-Region teilen, obwohl dies keineswegs der Beweis dafür ist, dass dies die Erklärung ist.

pdate: die Ausgabe von !dumpheap -stat bläst diese Theorie so ziemlich aus dem Wasser! Die Generation 2 und LOH haben ihre eigenen Regionen.

2

Hier sind einige Möglichkeiten, die genaue Aufruf-Stapel von LOH Zuordnung zu identifizieren.

Und um eine LOH-Fragmentierung zu vermeiden, weisen Sie eine große Anzahl von Objekten vorab zu und heften Sie sie fest. Verwenden Sie diese Objekte bei Bedarf erneut. Hier ist post zur LOH-Fragmentierung. So etwas könnte helfen, eine LOH-Fragmentierung zu vermeiden.

1
Naveen

Wenn das Format als Ihre Anwendung erkennbar ist, warum haben Sie den Code, der dieses Zeichenfolgenformat generiert, nicht identifiziert? Wenn es mehrere Möglichkeiten gibt, versuchen Sie, eindeutige Daten hinzuzufügen, um herauszufinden, welcher Codepfad der Schuldige ist.

Die Tatsache, dass die Arrays mit großen freigegebenen Elementen verschachtelt sind, lässt mich vermuten, dass sie ursprünglich gepaart oder zumindest verwandt waren. Versuchen Sie, die freigegebenen Objekte zu identifizieren, um herauszufinden, was sie generiert hat, und die zugehörigen Zeichenfolgen.

Versuchen Sie herauszufinden, was die Generierung dieser Zeichenfolgen verhindern würde. Vielleicht werden sie für Protokollierungszwecke oder ähnliches in eine vergessene oder nicht verwendete Liste gesteckt.


BEARBEITEN: Ignorieren Sie den Speicherbereich und die spezifische Arraygröße für den Moment: Stellen Sie einfach fest, was mit diesen Strings gemacht wird, um ein Leck zu verursachen. Probieren Sie die! GCRoot aus, wenn Ihr Programm diese Zeichenfolgen nur ein- oder zweimal erstellt oder bearbeitet hat, wenn weniger Objekte zu verfolgen sind.

1
HUAGHAGUAH

Tolle Frage, ich habe gelernt, indem ich die Fragen gelesen habe.

Ich denke, dass andere Teile des Deserialisierungs-Codepfads auch den großen Objekthaufen verwenden, daher die Fragmentierung. Wenn alle Saiten zur selben Zeit interniert wären, wäre das in Ordnung.

Wenn man bedenkt, wie gut der .net-Garbage-Collector ist, ist es wahrscheinlich gut genug, den Deserialisierungs-Codepfad nur ein normales Zeichenfolgenobjekt erstellen zu lassen. Machen Sie nichts Komplexeres, bis der Bedarf bewiesen ist.

Ich würde höchstens versuchen, eine Hash-Tabelle der letzten Saiten, die Sie gesehen haben, zu führen und diese wiederzuverwenden. Indem Sie die Größe der Hash-Tabelle begrenzen und die Größe beim Erstellen der Tabelle übergeben, können Sie die meisten Fragmentierungen stoppen. Anschließend müssen Sie Zeichenfolgen, die Sie in letzter Zeit nicht gesehen haben, aus der Hash-Tabelle entfernen, um die Größe zu begrenzen. Aber wenn die Zeichenfolgen, die der Deserialisierungscodepfad erstellt, ohnehin nur von kurzer Dauer sind, werden Sie nicht viel gewinnen, wenn überhaupt.

1
Ian Ringrose
1
Jose Gzz