Jedes Objekt, das die Kladde mit new anlegt, landet auf dem Heap, und irgendwann räumt es jemand weg: der Garbage Collector (GC), die Speicherbereinigung von .NET. Meistens merkt man von ihm nichts. Bemerkbar wird er in zwei Lagen: wenn eine App unter Last spürbar oft anhält, weil sie zu viel zuweist, und wenn ihr Speicher wächst, bis der Container sie beendet. Für die erste haben Sie in den Lektionen 3.12.1 und 3.12.2 Zuweisungen gemessen und gespart. Diese Lektion erklärt, was der GC mit ihnen macht, welche Arten es gibt, wie er sich in einem Container mit Speichergrenze verhält, und wie Sie ein Leck finden, bevor es die Kladde in Azure neu starten lässt.

Neu in dieser Lektion: nichts zu installieren. dotnet-counters und dotnet-gcdump kennen Sie aus Lektion 3.9.5, als lokale Werkzeuge im Repository der Kladde. Für die Übung mit Container brauchen Sie Docker Desktop oder Podman, wie in K2.

Generationen

Der GC geht von einer Beobachtung aus, die für fast alle Programme stimmt: Die meisten Objekte sterben jung. Ein String für eine Logzeile, eine Liste für eine Antwort, ein Objekt für eine Anfrage leben Millisekunden. Deshalb teilt er den Heap in Generationen:

  • Generation 0: Hier entsteht jedes neue Objekt. Ist das Budget der Generation voll, bereinigt der GC nur sie: Er sucht die Objekte, die noch jemand erreicht, und schiebt sie in die Generation 1. Alles andere ist frei. Weil fast alles tot ist, geht das schnell.
  • Generation 1: ein Puffer zwischen jung und alt. Was eine Bereinigung der Generation 1 überlebt, kommt in die Generation 2.
  • Generation 2: Objekte, die lange leben: Caches, Singletons, Konfiguration. Eine Bereinigung der Generation 2 (eine volle Bereinigung) sieht den ganzen Heap an und ist entsprechend teuer.
  • Der Heap für große Objekte (Large Object Heap): Objekte ab 85.000 Bytes, meist große Arrays. Sie werden nur mit der Generation 2 bereinigt und normalerweise nicht verschoben, weil das Kopieren teuer wäre. Viele große, kurzlebige Arrays sind deshalb ein bekanntes Muster für Ärger, und ArrayPool<T> aus Lektion 3.12.2 die übliche Abhilfe.

Während einer Bereinigung hält der GC die Threads der App kurz an. Für die Generationen 0 und 1 ist das meist sehr kurz, weil wenig überlebt. Die Generation 2 bereinigt er im Hintergrund, während die App weiterläuft, und hält nur am Anfang und am Ende kurz an.

Daraus folgen zwei Regeln für Ihren Code:

  • Kurzlebige Objekte sind billig, aber nicht kostenlos. Jede Zuweisung füllt das Budget der Generation 0, und jede volle Generation 0 heißt eine Bereinigung. Das ist die Spalte Gen0 in BenchmarkDotNet.
  • Mittellange Objekte sind am teuersten. Ein Objekt, das ein paar Sekunden lebt und dann stirbt, überlebt zwei Bereinigungen, wird zweimal kopiert und belastet am Ende die teure Generation 2. Ein Cache, dessen Einträge nach kurzer Zeit verfallen, kann genau so ein Muster sein.