Fragmentierung großer Objekthaufen

97

Die C # /. NET-Anwendung, an der ich arbeite, leidet unter einem langsamen Speicherverlust. 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, von denen ich erwarte, dass sie vorübergehend sind: Sobald die Berechnung abgeschlossen ist und an den Remote-Host gesendet wurde, sollte der Speicher freigegeben werden. Was ich jedoch sehe, ist eine große Anzahl von (Live-) Objektarrays, die mit freien Speicherblöcken verschachtelt sind, z. B. ein zufälliges Segment aus dem 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
...

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

Beachten Sie auch, dass CDB den Typ nicht meldet, wenn das Heap-Segment ausgegeben wird: Ich bin nicht sicher, ob dies zusammenhängt oder nicht. Wenn ich das markierte (<-) Objekt speichere, 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 anhand unseres Anwendungscodes 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 verlassen).

Ich würde es sehr begrüßen, wenn jemand Licht ins Dunkel bringen könnte, warum diese kleinen (<85k) Objektarrays auf dem LOH landen: In welchen Situationen wird .NET dort ein kleines Objektarray einfügen? Kennt jemand zufällig eine alternative Methode, um die Wurzeln dieser Objekte zu ermitteln?


Update 1

Eine andere Theorie, die ich gestern spät aufgestellt habe, ist, dass diese Objektarrays groß angefangen haben, aber verkleinert wurden, wobei die Blöcke des freien Speichers zurückbleiben, die in den Speicherabbildern erkennbar sind. Was mich misstrauisch macht, ist, dass die Objektarrays immer 1056 Bytes lang (128 Elemente), 128 * 8 für die Referenzen und 32 Bytes Overhead zu sein scheinen.

Die Idee ist, dass möglicherweise ein unsicherer Code in einer Bibliothek oder in der CLR die Anzahl der Elemente im Array-Header beschädigt. Ein bisschen weit hergeholt, ich weiß ...


Update 2

Dank Brian Rasmussen (siehe akzeptierte Antwort) wurde das Problem als Fragmentierung des LOH identifiziert, die durch die String-Intern-Tabelle verursacht wurde! 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 zuerst eindeutige Zeichenfolgen in einer Schleife. Dies soll nur beweisen, dass der Speicher in diesem Szenario nicht verloren geht. Offensichtlich sollte es nicht und es tut es nicht.

In der zweiten Schleife werden eindeutige Zeichenfolgen erstellt und interniert. Diese Aktion verwurzelt sie in der internen Tabelle. Was ich nicht realisiert habe ist, wie die interne Tabelle dargestellt wird. Es scheint, dass es aus einer Reihe von Seiten besteht - Objektarrays mit 128 Zeichenfolgenelementen -, die im LOH erstellt werden. Dies ist in CDB / SOS deutlicher:

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)

Ein Dump des LOH-Segments zeigt 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 Objektarraygröße 528 (anstelle von 1056) beträgt, da meine Workstation 32 Bit und der Anwendungsserver 64 Bit ist. Die Objektarrays sind immer noch 128 Elemente lang.

Die Moral dieser Geschichte ist es also, sehr vorsichtig zu internieren. Wenn nicht bekannt ist, dass die Zeichenfolge, die Sie internieren, Mitglied einer endlichen Menge ist, wird Ihre Anwendung aufgrund einer Fragmentierung des LOH zumindest in Version 2 der CLR auslaufen.

Im Fall unserer Anwendung enthält der Pfad des Deserialisierungscodes allgemeinen Code, der Entitätskennungen während des Unmarshalling interniert: Ich vermute jetzt stark, 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 verbleibt, wenn dieselbe Entität mehrmals deserialisiert wird.

Paul Ruane
quelle
2
Gute Frage - ich habe das Gleiche in meiner Bewerbung bemerkt. Kleine Objekte, die nach dem Reinigen der großen Blöcke im LOH verbleiben, verursachen Fragmentierungsprobleme.
Reed Copsey
2
Ich stimme zu, tolle Frage. Ich werde nach Antworten suchen.
Charlie Flowers
2
Sehr interessant. Klingt so, als wäre das Debuggen ein ziemliches Problem!
Matt Jordan

Antworten:

46

Die CLR verwendet die LOH, um einige Objekte vorab zuzuweisen (z. B. das Array, das für internierte Zeichenfolgen verwendet wird ). Einige davon sind weniger 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.

Aufgrund einer etwas esoterischen Optimierung werden double[]dem LOH auch 1000 oder mehr Elemente zugewiesen.

Brian Rasmussen
quelle
Die problematischen Objekte sind Objekte [], die Verweise auf Zeichenfolgen enthalten, von denen ich weiß, dass sie vom App-Code erstellt werden. Dies impliziert, dass die App die Objekte [] erstellt (ich kann keine Beweise dafür sehen) oder dass ein Teil der CLR (z. B. Serialisierung) sie verwendet, um an den Anwendungsobjekten zu arbeiten.
Paul Ruane
1
Dies könnte die interne Struktur sein, die für internierte Zeichenfolgen verwendet wird. Bitte überprüfen Sie meine Antwort auf diese Frage für weitere Details: stackoverflow.com/questions/372547/…
Brian Rasmussen
Ah, das ist eine sehr interessante Spur, danke. Den Praktikantentisch komplett vergessen. Ich weiß, dass einer unserer Entwickler ein begeisterter Interessent ist, daher werde ich dies definitiv untersuchen.
Paul Ruane
1
85000 Bytes oder 84 * 1024 = 87040 Bytes?
Peter Mortensen
5
85000 Bytes. Sie können dies überprüfen, indem Sie ein Byte-Array von 85000-12 (Längengröße, MT, Synchronisierungsblock) erstellen und GC.GetGenerationdie Instanz aufrufen . Dies gibt Gen2 zurück - die API unterscheidet nicht zwischen Gen2 und LOH. Wenn Sie das Array um ein Byte verkleinern, gibt die API Gen0 zurück.
Brian Rasmussen
13

Das .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 in GCSettings.LargeObjectHeapCompactionMode

Andre Abrantes
quelle
2

Wenn Sie Beschreibungen der Funktionsweise von GC und des Teils darüber lesen, wie langlebige Objekte in Generation 2 enden und die Sammlung von LOH-Objekten nur bei vollständiger Sammlung erfolgt, fällt Ihnen die Idee ein, ebenso wie die Sammlung von Generation 2. .. warum nicht einfach Generation 2 und große Objekte auf demselben 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 das LOH landen - wenn sie lange genug leben, um in Generation 2 zu landen.

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 LOH und Generation 2 dieselbe Heap-Region verwenden, obwohl dies keineswegs ein Beweis dafür ist, dass dies die Erklärung ist.

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

Daniel Earwicker
quelle
Verwenden Sie! Eeheap, um die Segmente anzuzeigen, aus denen jeder Heap besteht. Gen 0 und Gen 1 leben in einem Segment (demselben Segment), Gen 2 und LOH können beide mehrere Segmente zuordnen, aber diese Segmente für jeden Heap bleiben getrennt.
Paul Ruane
Ja, habe das gesehen, danke. Ich wollte nur den Befehl! Eeheaps erwähnen, da er dieses Verhalten viel deutlicher zeigt.
Paul Ruane
Die Effizienz des Haupt-GC beruht zu einem großen Teil auf der Tatsache, dass er Objekte verschieben kann, sodass sich nur eine kleine Anzahl freier Speicherbereiche auf dem Haupt-Heap befindet. Wenn ein Objekt auf dem Haupthaufen während einer Sammlung fixiert wird, muss der Bereich über und unter dem fixierten Objekt möglicherweise separat verfolgt werden. Da die Anzahl der fixierten Objekte normalerweise sehr gering ist, muss auch die Anzahl der separaten Bereiche, die der GC benötigt, sehr gering sein Spur. Das Mischen von verschiebbaren und nicht verschiebbaren (großen) Objekten auf demselben Heap würde die Leistung beeinträchtigen.
Supercat
Eine interessantere Frage ist, warum .NET doubleArrays mit mehr als 1000 Elementen auf dem LOH platziert, anstatt den GC zu optimieren, um sicherzustellen, dass sie an 8-Byte-Grenzen ausgerichtet sind. Selbst auf einem 32-Bit-System würde ich aufgrund des Cache-Verhaltens erwarten, dass das Auferlegen einer 8-Byte-Ausrichtung für alle Objekte, deren zugewiesene Größe ein Vielfaches von 8 Byte ist, wahrscheinlich einen Leistungsgewinn darstellt. Andernfalls double[]weiß ich nicht, warum die Größe mit der Nutzung korreliert , obwohl die Leistung eines stark genutzten , auf den Cache ausgerichteten Benutzers besser wäre als die eines nicht genutzten .
Supercat
@supercat Außerdem verhalten sich die beiden Heaps auch bei der Zuordnung sehr unterschiedlich. Der Haupthaufen ist (zu diesem Zeitpunkt) im Grunde genommen ein Stapel in Zuordnungsmustern - er wird immer oben zugewiesen, wobei jeglicher freier Speicherplatz ignoriert wird - wenn die Komprimierung erfolgt, werden die freien Speicherplätze herausgedrückt. Dies macht die Zuordnung fast zu einem No-Op und hilft bei der Datenlokalität. Auf der anderen Seite ähnelt die Zuweisung auf dem LOH der Funktionsweise von malloc - es wird der erste freie Platz gefunden, der das, was Sie zuweisen, aufnehmen und dort zuweisen kann. Da es sich um große Objekte handelt, ist die Datenlokalität eine Selbstverständlichkeit, und die Strafe für die Zuweisung ist nicht schlecht.
Luaan
1

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.

Wenn Sie herausgefunden haben, was diese Zeichenfolgen generiert, versuchen Sie herauszufinden, was sie davon abhält, GCed zu werden. Vielleicht werden sie in eine vergessene oder nicht verwendete Liste für Protokollierungszwecke oder ähnliches gestopft.


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

HUAGHAGUAH
quelle
Die Zeichenfolgen sind eine Mischung aus Guids (die wir verwenden) und Zeichenfolgenschlüsseln, die leicht zu identifizieren sind. Ich kann sehen, wo sie generiert werden, aber sie werden niemals (direkt) zu Objektarrays hinzugefügt, und wir erstellen nicht explizit 128 Elementarrays. Diese kleinen Arrays sollten sich jedoch zunächst nicht im LOH befinden.
Paul Ruane
1

Tolle Frage, ich habe durch das Lesen der Fragen gelernt.

Ich denke, dass andere Teile des Deserialisierungscodepfads auch den großen Objekthaufen verwenden, daher die Fragmentierung. Wenn alle Saiten zur gleichen Zeit interniert wären, wären Sie in Ordnung.

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

Ich würde höchstens versuchen, eine Hash-Tabelle der letzten Zeichenfolgen zu führen, die Sie gesehen haben, 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. Sie benötigen dann eine Möglichkeit, Zeichenfolgen, die Sie in letzter Zeit nicht gesehen haben, aus der Hash-Tabelle zu entfernen, um deren Größe zu begrenzen. Aber wenn die Zeichenfolgen, die der Deserialisierungscode-Pfad erstellt, ohnehin nur von kurzer Dauer sind, werden Sie nicht viel gewinnen, wenn überhaupt.

Ian Ringrose
quelle
1

Hier sind einige Möglichkeiten, um den genauen Aufrufstapel der LOH- Zuweisung zu ermitteln.

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

Naveen
quelle
Ich kann nicht verstehen, warum das Feststecken hier helfen sollte. Übrigens werden große Objekte auf LOH vom GC sowieso nicht bewegt. Es ist jedoch ein Implementierungsdetail.
user492238