Grundlegendes zur Speicherbereinigung in .NET


170

Betrachten Sie den folgenden Code:

public class Class1
{
    public static int c;
    ~Class1()
    {
        c++;
    }
}

public class Class2
{
    public static void Main()
    {
        {
            var c1=new Class1();
            //c1=null; // If this line is not commented out, at the Console.WriteLine call, it prints 1.
        }
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine(Class1.c); // prints 0
        Console.Read();
    }
}

Nun, obwohl die Variable c1 in der Hauptmethode außerhalb des Gültigkeitsbereichs liegt und beim Aufrufen von keinem anderen Objekt weiter referenziert GC.Collect()wird, warum wird sie dort nicht finalisiert?


8
Der GC gibt Instanzen nicht sofort frei, wenn sie außerhalb des Gültigkeitsbereichs liegen. Dies geschieht, wenn es dies für notwendig hält. Sie können alles über den GC hier lesen: msdn.microsoft.com/en-US/library/vstudio/0xy59wtx.aspx
user1908061

@ user1908061 (Pssst. Ihr Link ist defekt.)
Dragomok

Antworten:


352

Sie werden hier gestolpert und ziehen sehr falsche Schlussfolgerungen, weil Sie einen Debugger verwenden. Sie müssen Ihren Code so ausführen, wie er auf dem Computer Ihres Benutzers ausgeführt wird. Wechseln Sie zuerst mit Build + Configuration Manager zum Release-Build, und ändern Sie die Kombination "Aktive Lösungskonfiguration" in der oberen linken Ecke in "Release". Gehen Sie als Nächstes zu Extras + Optionen, Debuggen, Allgemein und deaktivieren Sie die Option "JIT-Optimierung unterdrücken".

Führen Sie nun Ihr Programm erneut aus und basteln Sie am Quellcode. Beachten Sie, dass die zusätzlichen Zahnspangen überhaupt keine Wirkung haben. Beachten Sie auch, dass das Festlegen der Variablen auf Null überhaupt keinen Unterschied macht. Es wird immer "1" gedruckt. Es funktioniert jetzt so, wie Sie es sich erhofft und erwartet haben.

Damit bleibt die Aufgabe zu erklären, warum es beim Ausführen des Debug-Builds so anders funktioniert. Dazu muss erklärt werden, wie der Garbage Collector lokale Variablen erkennt und wie sich dies auf das Vorhandensein eines Debuggers auswirkt.

Zunächst führt der Jitter zwei wichtige Aufgaben aus, wenn er die IL für eine Methode in Maschinencode kompiliert. Der erste ist im Debugger sehr gut sichtbar. Sie können den Maschinencode im Fenster Debug + Windows + Disassembly sehen. Die zweite Pflicht ist jedoch völlig unsichtbar. Außerdem wird eine Tabelle generiert, in der beschrieben wird, wie die lokalen Variablen im Methodenkörper verwendet werden. Diese Tabelle enthält einen Eintrag für jedes Methodenargument und jede lokale Variable mit zwei Adressen. Die Adresse, an der die Variable zuerst eine Objektreferenz speichert. Und die Adresse des Maschinencodebefehls, an dem diese Variable nicht mehr verwendet wird. Auch ob diese Variable im Stapelrahmen oder in einem CPU-Register gespeichert ist.

Diese Tabelle ist für den Garbage Collector von wesentlicher Bedeutung. Er muss wissen, wo bei der Durchführung einer Sammlung nach Objektreferenzen gesucht werden muss. Ziemlich einfach, wenn die Referenz Teil eines Objekts auf dem GC-Heap ist. Auf jeden Fall nicht einfach, wenn die Objektreferenz in einem CPU-Register gespeichert ist. Auf dem Tisch steht, wo man suchen muss.

Die "nicht mehr verwendete" Adresse in der Tabelle ist sehr wichtig. Dies macht den Müllsammler sehr effizient . Es kann eine Objektreferenz erfassen, auch wenn sie in einer Methode verwendet wird und die Ausführung dieser Methode noch nicht abgeschlossen ist. Was sehr häufig vorkommt, ist, dass Ihre Main () -Methode beispielsweise erst kurz vor dem Beenden Ihres Programms nicht mehr ausgeführt wird. Natürlich möchten Sie nicht, dass Objektreferenzen, die in dieser Main () -Methode verwendet werden, für die Dauer des Programms gültig sind, was einem Leck gleichkommt. Der Jitter kann anhand der Tabelle feststellen, dass eine solche lokale Variable nicht mehr nützlich ist, je nachdem, wie weit das Programm innerhalb dieser Main () -Methode vor dem Aufruf fortgeschritten ist.

Eine fast magische Methode, die mit dieser Tabelle zusammenhängt, ist GC.KeepAlive (). Es ist eine ganz besondere Methode, sie generiert überhaupt keinen Code. Die einzige Aufgabe besteht darin, diese Tabelle zu ändern. Es erstreckt sichdie Lebensdauer der lokalen Variablen, wodurch verhindert wird, dass die darin gespeicherte Referenz den Müll sammelt. Sie müssen es nur verwenden, um zu verhindern, dass der GC mit dem Sammeln einer Referenz zu eifrig wird. Dies kann in Interop-Szenarien auftreten, in denen eine Referenz an nicht verwalteten Code übergeben wird. Der Garbage Collector kann nicht sehen, dass solche Referenzen von einem solchen Code verwendet werden, da er nicht vom Jitter kompiliert wurde und daher nicht über die Tabelle verfügt, in der angegeben ist, wo nach der Referenz gesucht werden soll. Das Übergeben eines Delegatenobjekts an eine nicht verwaltete Funktion wie EnumWindows () ist das Beispiel dafür, wann Sie GC.KeepAlive () verwenden müssen.

Wie Sie Ihrem Beispiel-Snippet nach dem Ausführen im Release-Build entnehmen können , können lokale Variablen frühzeitig erfasst werden, bevor die Ausführung der Methode abgeschlossen ist. Noch stärker kann ein Objekt gesammelt bekommen , während eine seiner Methoden ausgeführt , wenn diese Methode nicht mehr bezieht sich dies . Es gibt ein Problem damit, es ist sehr umständlich, eine solche Methode zu debuggen. Da können Sie die Variable durchaus in das Überwachungsfenster einfügen oder überprüfen. Und es würde verschwinden, während Sie debuggen, wenn ein GC auftritt. Das wäre sehr unangenehm, daher ist sich der Jitter bewusst, dass ein Debugger angeschlossen ist. Es ändert sich danndie Tabelle und ändert die "zuletzt verwendete" Adresse. Und ändert es von seinem normalen Wert in die Adresse des letzten Befehls in der Methode. Dadurch bleibt die Variable am Leben, solange die Methode nicht zurückgegeben wurde. So können Sie es so lange beobachten, bis die Methode zurückkehrt.

Dies erklärt nun auch, was Sie zuvor gesehen haben und warum Sie die Frage gestellt haben. Es wird "0" ausgegeben, da der Aufruf von GC.Collect die Referenz nicht erfassen kann. Die Tabelle besagt, dass die Variable nach dem Aufruf von GC.Collect () bis zum Ende der Methode verwendet wird. Dies muss erzwungen werden, indem der Debugger angehängt und der Debug-Build ausgeführt wird.

Das Setzen der Variablen auf Null hat jetzt Auswirkungen, da der GC die Variable überprüft und keine Referenz mehr sieht. Aber stellen Sie sicher, dass Sie nicht in die Falle tappen, in die viele C # -Programmierer geraten sind. Das Schreiben dieses Codes war sinnlos. Es spielt keine Rolle, ob diese Anweisung vorhanden ist, wenn Sie den Code im Release-Build ausführen. In der Tat werden die Jitter - Optimierer entfernen diese Aussage , da sie keinerlei Auswirkungen haben. Schreiben Sie also keinen solchen Code, auch wenn dies Auswirkungen zu haben schien .


Ein letzter Hinweis zu diesem Thema: Dies bringt Programmierer in Schwierigkeiten, die kleine Programme schreiben, um etwas mit einer Office-App zu tun. Der Debugger bringt sie normalerweise auf den falschen Pfad. Sie möchten, dass das Office-Programm bei Bedarf beendet wird. Der geeignete Weg, dies zu tun, ist der Aufruf von GC.Collect (). Aber sie werden feststellen, dass es nicht funktioniert, wenn sie ihre App debuggen, und sie durch das Aufrufen von Marshal.ReleaseComObject () in ein Niemals-Niemals-Land führen. Manuelle Speicherverwaltung funktioniert selten ordnungsgemäß, da eine unsichtbare Schnittstellenreferenz leicht übersehen wird. GC.Collect () funktioniert tatsächlich, nur nicht, wenn Sie die App debuggen.


1
Siehe auch meine Frage, die Hans gut für mich beantwortet hat. stackoverflow.com/questions/15561025/…
Dave Nay

1
@HansPassant Ich habe gerade diese großartige Erklärung gefunden, die auch einen Teil meiner Frage hier beantwortet: stackoverflow.com/questions/30529379/… über GC- und Thread-Synchronisation. Eine Frage, die ich noch habe: Ich frage mich, ob der GC tatsächlich Adressen komprimiert und aktualisiert, die in einem Register verwendet werden (im Speicher gespeichert, während sie angehalten sind), oder sie einfach überspringt. Ein Prozess, der Register nach dem Anhalten des Threads (vor dem Lebenslauf) aktualisiert, fühlt sich für mich wie ein ernsthafter Sicherheitsthread an, der vom Betriebssystem blockiert wird.
atlaste

Indirekt ja. Der Thread wird angehalten, der GC aktualisiert den Sicherungsspeicher für die CPU-Register. Sobald der Thread wieder ausgeführt wird, werden die aktualisierten Registerwerte verwendet.
Hans Passant

1
@HansPassant, würde ich mich freuen, wenn Sie Referenzen für einige der nicht offensichtlichen Details des CLR-Garbage Collectors hinzufügen, die Sie hier beschrieben haben?
Denfromufa

In Bezug auf die Konfiguration scheint ein wichtiger Punkt zu sein, dass "Code optimieren" ( <Optimize>true</Optimize>in .csproj) aktiviert ist. Dies ist die Standardeinstellung in der Konfiguration "Release". Wenn Sie jedoch benutzerdefinierte Konfigurationen verwenden, ist es wichtig zu wissen, dass diese Einstellung wichtig ist.
Zero3

34

[Ich wollte nur den Internals of Finalization-Prozess weiter ergänzen]

Sie erstellen also ein Objekt und wenn das Objekt erfasst wird, sollte die FinalizeMethode des Objekts aufgerufen werden. Die Finalisierung beinhaltet jedoch mehr als diese sehr einfache Annahme.

KURZE KONZEPTE ::

  1. Objekte, die KEINE FinalizeMethoden implementieren , werden dort sofort zurückgefordert, es sei denn, sie sind natürlich nicht mehr über den
    Anwendungscode erreichbar

  2. Objekte der Umsetzung FinalizeMethode, das Konzept / Umsetzung von Application Roots, Finalization Queue, Freacheable Queuekommt , bevor sie freigegeben werden kann.

  3. Jedes Objekt wird als Müll betrachtet, wenn es NICHT über den Anwendungscode erreichbar ist

Angenommen: Klassen / Objekte A, B, D, G, H implementieren NICHT die FinalizeMethode und C, E, F, I, J implementieren die FinalizeMethode.

Wenn eine Anwendung ein neues Objekt erstellt, weist der neue Operator den Speicher aus dem Heap zu. Wenn der Objekttyp eine FinalizeMethode enthält , wird ein Zeiger auf das Objekt in die Finalisierungswarteschlange gestellt .

Daher werden Zeiger auf die Objekte C, E, F, I, J zur Finalisierungswarteschlange hinzugefügt.

Die Finalisierungswarteschlange ist eine interne Datenstruktur, die vom Garbage Collector gesteuert wird. Jeder Eintrag in der Warteschlange zeigt auf ein Objekt, dessen FinalizeMethode aufgerufen werden sollte, bevor der Speicher des Objekts zurückgefordert werden kann. Die folgende Abbildung zeigt einen Heap mit mehreren Objekten. Einige dieser Objekte sind von den Wurzeln der Anwendung aus erreichbarund einige nicht. Wenn Objekte C, E, F, I und J erstellt wurden, erkennt das .NET-Framework, dass diese Objekte FinalizeMethoden haben , und Zeiger auf diese Objekte werden der Finalisierungswarteschlange hinzugefügt .

Geben Sie hier die Bildbeschreibung ein

Wenn eine GC auftritt (1. Sammlung), werden die Objekte B, E, G, H, I und J als Müll bestimmt. Da A, C, D, F immer noch über den Anwendungscode erreichbar sind, der durch die Pfeile aus dem gelben Kästchen oben dargestellt ist.

Der Garbage Collector durchsucht die Finalisierungswarteschlange nach Zeigern auf diese Objekte. Wenn ein Zeiger gefunden wird, wird der Zeiger aus der Finalisierungswarteschlange entfernt und an die auslaugbare Warteschlange angehängt ("F-erreichbar").

Die freachable Warteschlange ist eine weitere interne Datenstruktur, die vom Garbage Collector gesteuert wird. Jeder Zeiger in der auslaugbaren Warteschlange identifiziert ein Objekt, dessen FinalizeMethode aufgerufen werden kann.

Nach der Sammlung (1. Sammlung) sieht der verwaltete Heap ähnlich aus wie in der folgenden Abbildung. Erläuterung unten:
1.) Der von den Objekten B, G und H belegte Speicher wurde sofort zurückgefordert, da diese Objekte keine Finalisierungsmethode hatten, die aufgerufen werden musste .

2.) Der von den Objekten E, I und J belegte Speicher konnte jedoch nicht zurückgefordert werden, da ihre FinalizeMethode noch nicht aufgerufen wurde. Das Aufrufen der Finalize-Methode erfolgt über eine auslaugbare Warteschlange.

3.) A, C, D, F sind weiterhin über den Anwendungscode erreichbar, der durch die Pfeile aus dem gelben Kästchen oben dargestellt ist. Sie werden daher auf keinen Fall gesammelt

Geben Sie hier die Bildbeschreibung ein

Es gibt einen speziellen Laufzeit-Thread zum Aufrufen von Finalize-Methoden. Wenn die Warteschlange leer ist (was normalerweise der Fall ist), wird dieser Thread in den Ruhezustand versetzt. Wenn jedoch Einträge angezeigt werden, wird dieser Thread aktiviert, entfernt jeden Eintrag aus der Warteschlange und ruft die Finalize-Methode jedes Objekts auf. Der Garbage Collector komprimiert den wiederherstellbaren Speicher und der spezielle Laufzeit-Thread leert die auslaugbare Warteschlange und führt die FinalizeMethode jedes Objekts aus . Hier ist also endlich, wenn Ihre Finalize-Methode ausgeführt wird

Wenn der Garbage Collector das nächste Mal aufgerufen wird (2nd Collection), stellt er fest, dass die finalisierten Objekte wirklich Garbage sind, da die Wurzeln der Anwendung nicht darauf verweisen und die auslaugbare Warteschlange nicht mehr darauf verweist (es ist auch LEER) Der Speicher für die Objekte (E, I, J) wird einfach aus dem Heap zurückgefordert. Siehe Abbildung unten und vergleichen Sie ihn mit der Abbildung oben

Geben Sie hier die Bildbeschreibung ein

Hier ist es wichtig zu verstehen, dass zwei GCs erforderlich sind, um den von Objekten, die finalisiert werden müssen, verwendeten Speicher zurückzugewinnen . In der Realität sind sogar mehr als zwei Sammlungen erforderlich, da diese Objekte möglicherweise zu einer älteren Generation befördert werden

HINWEIS :: Die auslaugbare Warteschlange wird als Root betrachtet, genau wie globale und statische Variablen Roots sind. Befindet sich ein Objekt in der Warteschlange, ist das Objekt erreichbar und kein Müll.

Denken Sie als letzten Hinweis daran, dass das Debuggen von Anwendungen eine Sache ist, Garbage Collection eine andere und anders funktioniert. Bisher können Sie die Speicherbereinigung nicht nur durch Debuggen von Anwendungen fühlen. Wenn Sie den Speicher untersuchen möchten, können Sie hier beginnen.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.