Magie, die Sie verstehen müssen
\n\nJeder, der mindestens ein paar Wochen in C# programmiert hat, weiß: Die Laufzeitumgebung (CLR) verwaltet den Speicher. Sie erstellen einfach Objekte mit new, und wenn sie nicht mehr benötigt werden, kommt der Garbage Collector (GC) und räumt alles auf. Klingt nach Zauberei, oder? Aber hinter dieser Magie steckt strenge Ingenieurskunst, und wenn man sie nicht versteht, wird Ihre Anwendung anfangen zu „ruckeln“, Gigabytes an RAM zu „fressen“ oder sogar mit einem OutOfMemoryException abstürzen.
In diesem Artikel werfen wir einen Blick unter die Haube der CLR und verstehen, wie genau die Speicherverwaltung in C# funktioniert. Sie erfahren, worin sich Stack und Heap unterscheiden, was Objektgenerationen sind, warum IDisposable Ihr bester Freund ist und wie Sie die Fallstricke mit dem Large Object Heap vermeiden. Los geht's!
Stack und Heap: Das Fundament, über das in Lehrbüchern geschwiegen wird
\n\nWenn Ihr Programm läuft, teilt die CLR den Speicher in zwei Hauptbereiche auf: den Stack und den Heap. Den Unterschied zu verstehen, ist die Grundlage, ohne die alles andere sinnlos wird.
\n\nDer Stack – schnell, aber klein
\nDer Stack ist ein Speicherbereich, in dem Value Types (int, bool, struct, enum) und Referenzen auf Objekte gespeichert werden. Er arbeitet nach dem LIFO-Prinzip (Last In, First Out). Wenn Sie eine Methode aufrufen, wird für sie ein Frame auf dem Stack reserviert. Sobald die Methode beendet ist, wird der Frame sofort zerstört. Hier ist kein Garbage Collector nötig. Die Zugriffsgeschwindigkeit auf den Stack ist maximal, aber die Größe ist begrenzt (normalerweise 1-4 MB pro Thread).
\n\nvoid Example()\n{\n int x = 42; // x liegt auf dem Stack\n string s = "Hallo"; // Referenz s auf dem Stack, das Objekt selbst – im Heap\n}\n\n\nDer Heap – langsamer, aber geräumiger
\nDer Heap (Managed Heap) ist der „langlebige“ Speicher. Hier leben alle Reference Types (Klassen, Strings, Arrays, Delegaten). Die Speicherzuweisung im Heap erfolgt bei jedem new. Und hier kommt der Garbage Collector ins Spiel. Er entscheidet, wann Speicher freigegeben werden kann und wann nicht.
Wichtig: Der Stack ist nicht „schlecht“ und der Heap nicht „gut“. Jeder hat seine eigene Aufgabe. Value Types sind schnell, aber sie können ohne das Schlüsselwort ref nicht als Referenz übergeben werden. Reference Types sind flexibel, benötigen aber den GC.
Wie der Garbage Collector in C# funktioniert: Generationen und Algorithmus
\n\nDer Garbage Collector in .NET ist nicht einfach „gelöscht und vergessen“. Es ist ein intelligenter Mechanismus, der Generationen verwendet, um die Pausen im Anwendungsbetrieb zu minimieren.
\n\nDrei Objektgenerationen
\n- \n
- Gen 0 – neugeborene Objekte. Sie leben kurz. Die Gen-0-Sammlung erfolgt häufig und sehr schnell. \n
- Gen 1 – „Überlebende“ nach einer Sammlung. Eine Pufferzone zwischen jungen und alten Objekten. \n
- Gen 2 – Langlebige Objekte. Die Gen-2-Sammlung ist die teuerste und seltenste. Wenn ein Objekt bis Gen 2 überlebt hat, wird es dort höchstwahrscheinlich lange bleiben. \n
Der Arbeitsalgorithmus des GC sieht vereinfacht wie folgt aus:
\n- \n
- Die Anwendung fordert Speicher an. Wenn im Heap kein Platz ist, wird eine Sammlung gestartet. \n
- Der GC erstellt einen Erreichbarkeitsgraphen von Objekten, beginnend mit den Wurzeln (globale statische Felder, lokale Variablen in Thread-Stacks, CPU-Register). \n
- Alle nicht erreichbaren Objekte werden als „Müll“ markiert. \n
- Es findet eine Kompaktierung statt – lebende Objekte werden verschoben, um Fragmentierung zu vermeiden. \n
- Der freigewordene Speicher wird an den Pool zurückgegeben. \n
Hier ist ein anschauliches Beispiel, wie der GC die Leistung beeinflusst:
\n\nclass HeavyObject\n{\n public byte[] Data = new byte[1024 * 1024]; // 1 MB\n}\n\nvoid TestGC()\n{\n for (int i = 0; i < 1000; i++)\n {\n var obj = new HeavyObject(); // Jedes Mal 1 MB im Heap zuweisen\n // obj verlässt den Gültigkeitsbereich – wird zu Müll\n }\n // Nach der Schleife kann der GC Gen 0 einsammeln