## 4.8 Das Zerstören von Objekten – der Garbage Collector ### 4.8.1 Arbeitsweise des Garbage Collectors Ein Konstruktor wird aufgerufen, wenn das Objekt einer Klasse erzeugt wird. Damit beginnt der Lebenszyklus des Objekts. Objekte benötigen Speicherressourcen für ihre Daten. Solange ein Objekt noch referenziert wird, müssen die Daten im Speicher bleiben. Verliert ein Objekt seine letzte Referenz oder wird der Objektreferenz null zugewiesen, beispielsweise mit
    Circle kreis = new Circle();
    ...
    kreis = null;
können die vom Objekt beanspruchten Speicherressourcen freigegeben werden. Das geschieht jedoch nicht automatisch. Vielmehr beanspruchen die Objekte weiterhin Speicher, obwohl sie vom laufenden Programm nicht mehr genutzt werden können. Unter .NET ist es, im Gegensatz zu anderen Programmierumgebungen, nicht möglich, mittels Programmcode den Speicher freizugeben. Stattdessen sorgt eine spezielle Komponente der Common Language Runtime (CLR) für die notwendige Speicherbereinigung: der Garbage Collector. Der Garbage Collector (GC) arbeitet nichtdeterministisch, das heißt, es kann nicht vorhergesagt werden, wann der Garbage Collector aktiv wird. Damit stellt sich sofort die Frage, nach welchen Kriterien der GC seine Arbeit aufnimmt und eine Speicherbereinigung durchführt. Als selbstständige Ausführungseinheit (Thread) genießt der GC keine hohe Priorität und kann erst dann den Prozessor in Anspruch nehmen, wenn die Anwendung beschäftigungslos ist. Theoretisch könnte das bedeuten, dass eine viel beschäftigte Anwendung dem GC keine Chance lässt, jemals aktiv zu werden. Dem ist tatsächlich so, es gibt aber eine wichtige Einschränkung: Noch bevor den Speicherressourcen der Anwendung die Luft ausgeht, ist die zweite Bedingung erfüllt, um die Speicherbereinigung mit dem GC anzustoßen. Der Garbage Collector wird also spätestens dann nach allen aufgegebenen Objekten suchen und deren Speicherplatz freigeben, wenn die Speicherressourcen knapp werden.
Hinweis
Der Garbage Collector ist nur im Zusammenhang mit Referenztypen von Bedeutung. Daten, die auf Wertetypen basieren (und ihr Dasein auf dem Stack fristen), hören automatisch auf zu existieren, wenn die Methode verlassen wird, in der sie als lokale Variablen definiert sind. Handelt es sich um Instanzfelder, werden die Daten beim Zerstören des Objekts aufgegeben.
Die Arbeit des Garbage Collectors ist sehr zeitintensiv, da sich im Hintergrund sehr viele einzelne Aktivitäten abspielen. Dabei werden beispielsweise Objekte in andere Bereiche des Heaps kopiert und die entsprechenden Verweise auf diese Objekte aktualisiert. Damit nicht unnötig viel Zeit vom Garbage Collector beansprucht wird, ist der Speicherbereinigungsprozess ein ziemlich ausgeklügeltes System. Unter anderem werden die Objekte in drei separate Speicherbereiche aufgeteilt, die als Generationen bezeichnet werden. Das Konzept der Speicherbereinigung unter Berücksichtigung der Generationen ist dabei wie folgt: Generation 0 bleibt den neuen Objekten vorbehalten. Ist dieser Speicherbereich voll, wird der Garbage Collector aktiv und gibt die Speicherressourcen der nicht mehr benötigten Objekte frei. Objekte der Generation 0, die weiter referenziert werden, werden in den Bereich der Generation 1 kopiert. Sollte der erste Vorgang nicht genügend Speicherressourcen freigesetzt haben, erfasst der Garbage Collector auch den Bereich der Objekte, die der Generation 1 zugeordnet sind. Objekte, die dort nicht mehr referenziert werden, werden gelöscht. Alle anderen werden in den Bereich der Generation 2 verschoben. Reicht auch danach der Speicher immer noch nicht aus, wird der Garbage Collector auch alle nicht mehr benötigten Objekte der Generation 2 löschen. Die Idee, die hinter dem Prinzip der Generationen steckt, beruht darauf, dass die meisten Objekte nur für eine relativ kurze Zeitspanne benötigt werden. Je älter aber ein Objekt ist, umso größer ist die Wahrscheinlichkeit, dass es auch weiterhin benötigt wird. Das ist der Grund, warum der Garbage Collector sich zuerst um die Objekte der Generation 0 kümmert und nur dann die Objekte der Generation 1 und eventuell auch die der Generation 2 erfasst, wenn die freigegebenen Ressourcen anschließend immer noch nicht ausreichend sind. ### 4.8.2 Expliziter Aufruf des Garbage Collectors Sie können zwar mittels Code nicht die Speicherressourcen eines einzelnen Objekts freigeben, aber immerhin können Sie veranlassen, dass der Garbage Collector aktiv wird. Dazu wird die statische Methode Collect der Klasse GC aufgerufen:
    GC.Collect();
Dieser Aufruf veranlasst den Garbage Collector, alle drei Generationen zu bereinigen. Sie können den Aufruf optimieren, indem Sie der Methode mitteilen, welche die letzte noch zu bereinigende Generation sein soll. Mit
    GC.Collect(1);
erreichen Sie, dass die verwaisten Objekte der Generationen 0 und 1 zerstört werden. Die Klasse GC eignet sich auch, um in Erfahrung zu bringen, welcher Generation ein bestimmtes Objekt zugeordnet ist. Rufen Sie dazu die statische Methode GetGeneration auf, wobei Sie das Objekt übergeben, das abgefragt werden soll:
    int generation = GC.GetGeneration(kreis);
### 4.8.3 Der Destruktor Der Garbage Collector sorgt dafür, dass der Speicherplatz nicht mehr referenzierter Objekte freigegeben wird. Es gibt aber auch Objekte, die ihrerseits Referenzen auf externe Fremdressourcen halten. Dabei kann es sich zum Beispiel um Datenbankverbindungen oder geöffnete Dateien handeln. Solche Fremdressourcen werden vom Garbage Collector nicht verwaltet und konsequenterweise auch nicht freigegeben. In solchen Fällen sind die Objekte selbst dafür verantwortlich. Ein zweistufiges Modell unterstützt Sie bei der Freigabe der Fremdressourcen: der Destruktor die Schnittstelle IDisposable Widmen wir uns zuerst dem Destruktor, dessen Syntax wie folgt lautet:
    ~Klassenbezeichner() {/*...*/}
Eingeleitet wird der Destruktor mit dem Tildezeichen, danach folgen der Klassenbezeichner mit dem obligatorischen runden Klammerpaar und zum Schluss der Anweisungsblock. Ein Destruktor hat weder einen Zugriffsmodifizierer noch eine Parameterliste, und es wird auch kein Rückgabetyp angegeben. Der C#-Compiler wandelt den Destruktor in eine Überschreibung der von Object geerbten Methode Finalize um und markiert das Objekt gleichzeitig als finalisierungsbedürftig. Aus dem Programmcode heraus kann der Destruktor nicht aufgerufen werden. Das kann nur der Garbage Collector bei seinen Aufräumarbeiten. Trifft der Garbage Collector auf ein verwaistes und finalisierungsbedürftiges Objekt, erzeugt er einen neuen Objektverweis und stellt danach das Objekt in eine Finalisierungswarteschlange. Ein separater Thread arbeitet diese Warteschlange ab, ruft die Methode Finalize auf und markiert das Objekt. Erst beim nächsten Speicherbereinigungsprozess wird das Objekt komplett entfernt und dessen Speicherplatz freigegeben. Der gesamte Vorgang ist sehr aufwendig und muss mit einer Leistungseinbuße bezahlt werden. Sie sollten daher nur dann einen Destruktor bereitstellen, wenn er tatsächlich benötigt wird. ### 4.8.4 Die »IDisposable«-Schnittstelle Mit einem Destruktor sind zwei gravierende Nachteile verbunden: Wenn ein Destruktor implementiert ist, kann nicht exakt vorherbestimmt werden, wann er vom Speicherbereinigungsprozess ausgeführt wird. Ein Destruktor kann nicht explizit aus dem Code heraus aufgerufen werden. Wie Sie bereits wissen, werden die Aufräumarbeiten angestoßen, wenn durch die Beschäftigungslosigkeit einer laufenden Anwendung der niedrig priorisierte Thread des Garbage Collectors seine Arbeit aufnimmt oder sich die Speicherressourcen verknappen. Tatsächlich sind sogar Situationen denkbar, die niemals zum Destruktoraufruf führen – denken Sie nur an den Absturz des Rechners. Folglich kann auch nicht garantiert werden, dass der GC überhaupt jemals seine ihm zugedachte Aufgabe verrichtet. Wenn ein Objekt aber kostspielige oder begrenzte Ressourcen beansprucht, muss sichergestellt sein, dass diese so schnell wie möglich wieder freigegeben werden. Um dem Problem zu begegnen, können Sie, auch zusätzlich zum Destruktor, eine öffentliche Methode implementieren, die der Benutzer der Klasse explizit aufrufen kann. Grundsätzlich kann dazu jede beliebige Methode geschrieben werden, jedoch empfiehlt es sich, die Schnittstelle IDisposable zu implementieren, die die Methode Dispose vorschreibt.
Hinweis
Es ist unüblich, anstelle der Methode Dispose eine Methode Close zu definieren. Trotzdem weisen viele Klassen in der .NET-Klassenbibliothek die Methode Close auf, in der aber in der Regel Dispose aufgerufen wird.
Der Destruktor und Dispose müssen miteinander harmonieren, denn sie sind voneinander abhängig. Deshalb sollten Sie bei der Codierung von Destruktor und Dispose auf die folgenden Punkte achten: Alle Fremdressourcen sollten von Dispose und im Destruktor freigegeben werden. Ruft der Code Dispose auf einem Objekt auf, werden die Fremdressourcen in Dispose freigegeben. In diesem Fall ist der Aufruf des Destruktors während des Finalisierungsprozesses unnötig und sollte vermieden werden, um einerseits unnötige Performance-Verluste zu vermeiden und andererseits auch mögliche Fehlerquellen im Keim zu ersticken. Dazu wird in Dispose die statische Methode SuppressFinalize der Klasse GC unter Angabe des betroffenen Objekts aufgerufen. Die Folge ist, dass das Objekt nicht mehr in die Finalisierungswarteschlange gestellt und der Destruktor nicht aufgerufen wird. Für den Fall, dass die Methode Dispose nicht explizit aufgerufen wird, sollte der Aufruf aus dem Destruktor heraus erfolgen. Im Destruktor dürfen nur externe, nicht verwaltete Ressourcen freigegeben werden. Das bedeutet andererseits, dass Felder, die auf Referenztypen basieren, nur in Dispose freigegeben werden dürfen (z. B. durch Setzen auf null oder durch den Aufruf der Dispose-Methode dieser Objekte). Der Grund ist darin zu finden, dass beim Aufruf von Dispose noch alle Objekte über einen Verweis erreichbar sind, bei der Ausführung des Destruktors jedoch nicht mehr. Möglicherweise sollte die Methode Dispose der Basisklasse aufgerufen werden. Sie sollten sicherstellen, dass Dispose auch bei einem mehrfachen Aufruf ohne Fehler reagiert. Ein Codemuster, das den Anforderungen der genannten Punkte genügt, wird üblicherweise wie folgt implementiert:
    public class Demo : IDisposable {
      bool disposed = false;
      public void Dispose() {
        // wird nur beim ersten Aufruf ausgeführt
        if (!disposed) {
          Dispose(true);
          GC.SuppressFinalize(this);
          disposed = true;
        }
      }
      protected virtual void Dispose(bool disposing) {
        if (disposing) {
          // Freigabe verwalteter Objekte
        }
        // Freigabe von Fremdressourcen
      }
      // Destruktor
      ~Demo() {
        Dispose(false);
      }
    }
Neben der parameterlosen Methode Dispose, die aus der Schnittstelle IDisposable stammt, und dem Konstruktor enthält die Klasse eine zweite, überladene Dispose-Methode. In dieser ist die Freigabe der Fremdressourcen und die Freigabe eventuell vorhandener verwalteter Objekte codiert. Wie bereits oben beschrieben wurde, dürfen verwaltete Ressourcen während der Destruktorausführung nicht mehr freigegeben werden. Daher wird die überladene Dispose-Methode vom Destruktor durch Übergabe von false und die parameterlose Dispose-Methode durch Übergabe von true aufgerufen. Der Wert des booleschen Parameters wird ausgewertet und dazu benutzt, festzustellen, um welchen Aufrufer es sich handelt. Nach der Sondierung werden Fremdressourcen und verwaltete Ressourcen entsprechend bereinigt. #### Die »using«-Anweisung zur Zerstörung von Objekten C# stellt eine alternative Möglichkeit bereit, um ein Objekt schnellstmöglich zu zerstören. Es handelt sich hierbei um das Schlüsselwort using, das in diesem Fall nicht als Direktive, sondern als Anweisung eingesetzt wird.
    using (Demo obj = new Demo()) {
      obj.AnyOperation();
    }
Im Ausdruck wird ein Objekt instanziiert, auf dem nach dem Verlassen des Anweisungsblocks automatisch die Dispose-Methode aufgerufen wird. Die Klasse, die im Ausdruck instanziiert wird, muss nur eine Bedingung erfüllen: Sie muss die Schnittstelle IDisposable implementieren. ### 4.8.5 Ergänzungen in den Klassen »Circle« und »Rectangle« Zum Abschluss der Betrachtungen zur Objektzerstörung sollen noch die beiden Klassen Circle und Rectangle überarbeitet werden. Bisher ist es nämlich so, dass die Objektzähler zwar erhöht, aber bei Aufgabe eines Objekts nicht reduziert werden. Wir implementieren daher in beiden Klassen die IDisposable-Schnittstelle und den jeweiligen Destruktor. Nachfolgend wird das exemplarisch für die Klasse Circle gezeigt.
    public class Circle : GeometricObject, IDisposable {
      private bool disposed;
      public void Dispose() {
        if (!disposed) {
          countCircles--;
          GC.SuppressFinalize(this);
          disposed = true;
        }
      }
      ~Circle() {
        Dispose();
      }
    }
Obwohl nun der Destruktor und Dispose dafür sorgen, den Objektzähler bei der Freigabe eines Objekts zu reduzieren, müssen Sie sich der Tatsache bewusst sein, dass Sie zu keinem Zeitpunkt garantieren können, dass der Zähler den richtigen Stand hat. Sie bleiben weiter darauf angewiesen, dass die Dispose-Methode explizit aufgerufen wird oder der Garbage Collector aktiv wird.
Hinweis
Das komplette Beispiel des Projekts GeometricObjects finden Sie auf der Buch-DVD unter Beispiele\Kapitel 4\GeometricObjectsSolution_3.