Suche…


Einführung

In .Net werden Objekte, die mit new () erstellt wurden, auf dem verwalteten Heapspeicher zugewiesen. Diese Objekte werden von dem Programm, das sie verwendet, niemals explizit abgeschlossen. Stattdessen wird dieser Prozess vom .Net Garbage Collector gesteuert.

Einige der folgenden Beispiele sind "Laborfälle", um den Garbage Collector bei der Arbeit und einige wichtige Details seines Verhaltens zu zeigen, während andere sich darauf konzentrieren, wie Klassen für die korrekte Handhabung durch den Garbage Collector vorbereitet werden.

Bemerkungen

Der Garbage Collector zielt darauf ab, die Programmkosten in Bezug auf den zugewiesenen Speicher zu senken, dies hat jedoch Kosten in Bezug auf die Verarbeitungszeit. Um einen guten Gesamtkompromiss zu erreichen, gibt es eine Reihe von Optimierungen, die bei der Programmierung mit dem Garbage Collector berücksichtigt werden sollten:

  • Wenn die Collect () - Methode explizit aufgerufen werden soll (was ohnehin selten der Fall sein sollte), sollten Sie den "optimierten" Modus in Betracht ziehen, der das Dead-Objekt nur dann finalisiert, wenn tatsächlich Speicher benötigt wird
  • Anstatt die Collect () - Methode aufzurufen, sollten Sie die AddMemoryPressure () - und RemoveMemoryPressure () - Methode verwenden, die nur dann eine Speichersammlung auslöst, wenn sie tatsächlich benötigt wird
  • Es kann nicht garantiert werden, dass eine Speichersammlung alle toten Objekte abschließt. Stattdessen verwaltet der Garbage Collector 3 "Generationen", ein Objekt, das manchmal von einer Generation in die nächste "überlebt"
  • Abhängig von verschiedenen Faktoren, einschließlich der Feinabstimmung der Einrichtung, können mehrere Threading-Modelle angewendet werden, die zu unterschiedlichen Interferenzen zwischen dem Garbage Collector-Thread und den anderen Anwendungsthread (en) führen.

Ein einfaches Beispiel für die (Müll-) Sammlung

In der folgenden Klasse gegeben:

public class FinalizableObject 
{
    public FinalizableObject()
    {
        Console.WriteLine("Instance initialized");
    }

    ~FinalizableObject()
    {
        Console.WriteLine("Instance finalized");
    }
}

Ein Programm, das eine Instanz erstellt, auch ohne sie zu verwenden:

new FinalizableObject(); // Object instantiated, ready to be used

Erzeugt die folgende Ausgabe:

<namespace>.FinalizableObject initialized

Wenn nichts anderes passiert, wird das Objekt erst abgeschlossen, wenn das Programm beendet ist (wodurch alle Objekte auf dem verwalteten Heap freigegeben werden, wodurch diese im Prozess abgeschlossen werden).

Es ist möglich, den Garbage Collector wie folgt an einem bestimmten Punkt auszuführen:

new FinalizableObject(); // Object instantiated, ready to be used
GC.Collect();

Welches führt zu folgendem Ergebnis:

<namespace>.FinalizableObject initialized
<namespace>.FinalizableObject finalized

Sobald der Garbage Collector aufgerufen wurde, wurde dieses Mal das nicht verwendete (auch als "tot") Objekt abgeschlossen und aus dem verwalteten Heap freigegeben.

Lebende Objekte und tote Objekte - die Grundlagen

Faustregel: Wenn eine Speicherbereinigung auftritt, sind "lebende Objekte" diejenigen, die noch verwendet werden, während "tote Objekte" solche sind, die nicht mehr verwendet werden. .

Im folgenden Beispiel (der Einfachheit halber sind FinalizableObject1 und FinalizableObject2 Unterklassen von FinalizableObject aus dem obigen Beispiel und erben daher das Verhalten der Initialisierungs- / Finalisierungsnachrichten):

var obj1 = new FinalizableObject1(); // Finalizable1 instance allocated here
var obj2 = new FinalizableObject2(); // Finalizable2 instance allocated here
obj1 = null; // No more references to the Finalizable1 instance 
GC.Collect();

Die Ausgabe wird sein:

<namespace>.FinalizableObject1 initialized
<namespace>.FinalizableObject2 initialized
<namespace>.FinalizableObject1 finalized

Zum Zeitpunkt, zu dem der Garbage Collector aufgerufen wird, ist FinalizableObject1 ein totes Objekt und wird finalisiert, während FinalizableObject2 ein Live-Objekt ist und auf dem verwalteten Heap verbleibt.

Mehrere tote Objekte

Was ist, wenn zwei (oder mehrere) ansonsten tote Objekte sich aufeinander beziehen? Dies wird im folgenden Beispiel gezeigt. Angenommen, OtherObject ist eine öffentliche Eigenschaft von FinalizableObject:

var obj1 = new FinalizableObject1(); 
var obj2 = new FinalizableObject2();
obj1.OtherObject = obj2;
obj2.OtherObject = obj1;
obj1 = null; // Program no longer references Finalizable1 instance
obj2 = null; // Program no longer references Finalizable2 instance
// But the two objects still reference each other
GC.Collect();

Dies erzeugt die folgende Ausgabe:

<namespace>.FinalizedObject1 initialized
<namespace>.FinalizedObject2 initialized
<namespace>.FinalizedObject1 finalized
<namespace>.FinalizedObject2 finalized

Die beiden Objekte werden finalisiert und aus dem verwalteten Heap freigegeben, obwohl sie sich gegenseitig referenzieren (da zu keinem Objekt ein anderer Verweis von einem tatsächlich aktiven Objekt vorhanden ist).

Schwache Referenzen

Schwache Verweise sind ... Verweise auf andere Objekte (auch als "Ziele" bezeichnet), aber "schwach", da sie die Sammlung von Objekten nicht verhindern. Mit anderen Worten, schwache Referenzen zählen nicht, wenn der Garbage Collector Objekte als "live" oder "dead" bewertet.

Der folgende Code:

var weak = new WeakReference<FinalizableObject>(new FinalizableObject());
GC.Collect();

Erzeugt die Ausgabe:

<namespace>.FinalizableObject initialized
<namespace>.FinalizableObject finalized

Das Objekt wird vom verwalteten Heap freigegeben, obwohl es von der WeakReference-Variablen referenziert wird (noch im Gültigkeitsbereich, wenn der Garbage-Collector aufgerufen wurde).

Konsequenz # 1: Es ist zu jedem Zeitpunkt unsicher, anzunehmen, ob ein WeakReference-Ziel auf dem verwalteten Heap noch zugewiesen ist oder nicht.

Konsequenz # 2: Immer wenn ein Programm auf das Ziel einer Schwachstellenreferenz zugreifen muss, sollte Code für beide Fälle bereitgestellt werden, wobei das Ziel noch zugewiesen ist oder nicht. Die Methode für den Zugriff auf das Ziel ist TryGetTarget:

var target = new object(); // Any object will do as target
var weak = new WeakReference<object>(target); // Create weak reference
target = null; // Drop strong reference to the target

// ... Many things may happen in-between

// Check whether the target is still available
if(weak.TryGetTarget(out target))
{
    // Use re-initialized target variable
    // To do whatever the target is needed for
}
else
{
    // Do something when there is no more target object
    // The target variable value should not be used here
}

Die generische Version von WeakReference ist seit .Net 4.5 verfügbar. Alle Framework-Versionen bieten eine nicht generische, nicht typisierte Version, die auf dieselbe Weise erstellt und wie folgt geprüft wird:

var target = new object(); // Any object will do as target
var weak = new WeakReference(target); // Create weak reference
target = null; // Drop strong reference to the target

// ... Many things may happen in-between

// Check whether the target is still available
if (weak.IsAlive)
{
    target = weak.Target;

    // Use re-initialized target variable
    // To do whatever the target is needed for
}
else
{
    // Do something when there is no more target object
    // The target variable value should not be used here
}

Entsorgen () vs. Finalisierer

Implementieren Sie die Dispose () - Methode (und deklarieren Sie die enthaltende Klasse als IDisposable), um sicherzustellen, dass speicherintensive Ressourcen freigegeben werden, sobald das Objekt nicht mehr verwendet wird. Der "Haken" ist, dass es keine starke Garantie gibt, dass die Dispose () -Methode jemals aufgerufen wird (im Gegensatz zu Finalizern, die immer am Ende der Lebensdauer des Objekts aufgerufen werden).

Ein Szenario ist ein Programm, das Dispose () für explizit erstellte Objekte aufruft:

private void SomeFunction()
{
    // Initialize an object that uses heavy external resources
    var disposableObject = new ClassThatImplementsIDisposable();

    // ... Use that object

    // Dispose as soon as no longer used
    disposableObject.Dispose();

    // ... Do other stuff 

    // The disposableObject variable gets out of scope here
    // The object will be finalized later on (no guarantee when)
    // But it no longer holds to the heavy external resource after it was disposed
}

Ein anderes Szenario deklariert eine Klasse, die vom Framework instanziiert werden soll. In diesem Fall erbt die neue Klasse normalerweise eine Basisklasse, beispielsweise erstellt sie in MVC eine Controller-Klasse als Unterklasse von System.Web.Mvc.ControllerBase. Wenn die Basisklasse die IDisposable-Schnittstelle implementiert, ist dies ein guter Hinweis darauf, dass Dispose () ordnungsgemäß vom Framework aufgerufen wird - es gibt jedoch keine starke Garantie.

Daher ist Dispose () kein Ersatz für einen Finalizer. stattdessen sollten die beiden für unterschiedliche Zwecke verwendet werden:

  • Ein Finalizer setzt schließlich Ressourcen frei, um Speicherverluste zu vermeiden, die andernfalls auftreten würden
  • Dispose () gibt Ressourcen frei (möglicherweise dieselben), sobald diese nicht mehr benötigt werden, um die allgemeine Speicherzuordnung zu entlasten.

Ordnungsgemäße Entsorgung und Fertigstellung von Objekten

Da Dispose () und Finalizer auf unterschiedliche Zwecke abzielen, sollte eine Klasse, die externe speicherintensive Ressourcen verwaltet, beide implementieren. Die Folge ist, die Klasse so zu schreiben, dass sie zwei mögliche Szenarien gut handhabt:

  • Wenn nur der Finalizer aufgerufen wird
  • Wenn zuerst Dispose () aufgerufen wird und später auch der Finalizer

Eine Lösung besteht darin, den Bereinigungscode so zu schreiben, dass ein ein- oder zweimaliges Ausführen das gleiche Ergebnis liefert wie das einmalige Ausführen. Die Machbarkeit hängt von der Art der Bereinigung ab, zum Beispiel:

  • Das Schließen einer bereits geschlossenen Datenbankverbindung hat wahrscheinlich keine Auswirkungen und funktioniert daher
  • Das Aktualisieren einiger "Verbrauchszähler" ist gefährlich und führt bei zweimaligem Aufruf zu einem falschen Ergebnis.

Eine sicherere Lösung stellt durch Entwurf sicher, dass der Bereinigungscode unabhängig vom externen Kontext einmalig aufgerufen wird. Dies kann auf "klassische Weise" mit einer eigenen Flagge erreicht werden:

public class DisposableFinalizable1: IDisposable
{
    private bool disposed = false;

    ~DisposableFinalizable1() { Cleanup(); }

    public void Dispose() { Cleanup(); }

    private void Cleanup()
    {
        if(!disposed)
        {
            // Actual code to release resources gets here, then
            disposed = true;
        }
    }
}

Alternativ bietet der Garbage Collector eine bestimmte Methode SuppressFinalize (), die das Überspringen des Finalizers ermöglicht, nachdem Dispose aufgerufen wurde:

public class DisposableFinalizable2 : IDisposable
{
    ~DisposableFinalizable2() { Cleanup(); }

    public void Dispose()
    {
        Cleanup();
        GC.SuppressFinalize(this);
    }

    private void Cleanup()
    {
        // Actual code to release resources gets here
    }
}


Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow