Merkwürdigkeiten beim .Net Garbage Collector


Thomas Wölfer
Thomas Wölfer

14. November 2008


Irgendwie scheint der .Net 2.0 Garbage-Collector einige Dinge anders zu tun, als ich bisher gedacht habe. Vermutlich liegt das daran, das ich irgendwas falsch mache, aber zur Zeit sieht es eher danach aus, das er auf einer Single-Core Maschine nicht richtig funktioniert... Zur Erklärung hier ein paar Graphiken:

Die drei Graphiken geben den Informationen über den Speicherverbrauch der Baustatik im Zuge von etwa 350 durchgeführten Testcases wieder. Jedes Pixel in X steht dabei für einen Testcase. (Die dritte Graphik ist ein bisschen kürzer, weil aufgrund von Speichermangel nicht alle Testcases durchgeführt werden können - das Programm stürzt mit einer OutOfMemory Exception ab.)

Das Programm arbeitet also eine Reihe von Fällen ab. Am Ende jedes Falles werden 3 Daten erhoben:

  • Die Speicherauslastung bezogen auf den verfügbaren Speicher. (Blaue Kurve)
  • Das Working-Set (Grüne Kurve [ Environment.WorkingSet ])
  • Die Speichermenge, die der GarbageCollector verwaltet. (Rote Kurve [GC.GetTotalMemory(false) ])

In allen drei Fällen wurde das exakt gleiche Binary und die exakt gleichen Testcases durchgeführt. Die erste Graphik kam dabei auf einem Quad-Core System zustande, die zwei auf einem Dual-Core und die dritte auf einem Single-Core System. Wie man leicht sehen kann, gibt es einen unmittelbaren Zusammenhang zwischen dem Working-Set und dem GC-Speicher. Was man auch sehen kann ist die Tatsache, das sich das Programm auf den beiden Multi-Core Systemen exakt so verhält, wie man es erwarten würde: Zwar gibt es an einigen Stellen ein Wachstum im Speicherbedarf, am im großen und ganzen ist der konstant - durchbrochen von leichten Sprüngen, die daraus resultieren, das der GC hin- und wieder eingreift und Speicher freigibt.

Soweit, sogut. Nicht so gut: Auf der Single-Core Maschine wird offensichtlich überhaupt kein Speicher freigegeben. Es wird immer mehr und mehr alloziert, bis das Programm schließlich per OutOfMemory Exception stirbt.

Nun war eine erste (nicht ganz unplausible) Vermutung die, das der GC-Thread auf der Single-Core Maschine einfach nie "dran" kam, weil das durchführen der Testcases einfach zu viel Last machte. Darum habe ich zu Testzwecken nach der Durchführung jedes Testcases und vor dem ermitteln der Speicherauslastung noch folgenden Code eingebaut:

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

Das sollte nach meinem dafürhalten und auf Basis der Dokumentation dazu führen, das auf jeden Fall ein Collect über alle Generationen durchgeführt und auch Finalizer aufgerufen werden. Und auf den Multicore-Systemen hat das auch die erwarteten Auswirkungen: (Hier nur eines der beiden Bilder)

Wenn man hier den Speicher ansieht, der dem GC unterliegt (rote "Kurve"), dann verhält sich er so wie man das erwarten würden: Nach jedem Collect() wird die gleiche Menge benötigt. Dummerweise sieht das auf der Single-Core Maschine anders aus:

Hier wird weiter fröhlich Speicher alloziert und nicht wieder freigegeben - und zwar noch schneller, als ohne das Collect() - die Anwendung stürzt noch deutlich früher mit einer OutOfMemory Exception ab.

Wäre sehr dankbar, wenn mir das jemand erklären könnte.