it-swarm.com.de

Warum fegt Garbage Collection nur den Haufen?

Grundsätzlich habe ich bisher gelernt, dass die Garbage Collection jede Datenstruktur, auf die derzeit nicht verwiesen wird, für immer löscht. Dies überprüft jedoch nur den Heap auf solche Bedingungen.

Warum wird nicht auch der Datenabschnitt (Globale, Konstanten usw. usw.) oder der Stapel überprüft? Was ist mit dem Haufen, dass es das einzige ist, was wir Müll sammeln wollen?

28
Dark Templar

Der Garbage Collector tut scannt den Stapel - um zu sehen, welche Dinge im Heap derzeit von Dingen auf dem Stapel verwendet werden (auf die verwiesen wird).

Für den Garbage Collector ist es nicht sinnvoll, das Sammeln des Stapelspeichers in Betracht zu ziehen, da der Stapel nicht auf diese Weise verwaltet wird: Alles auf dem Stapel wird als "in Verwendung" betrachtet. Der vom Stapel verwendete Speicher wird automatisch zurückgefordert, wenn Sie von Methodenaufrufen zurückkehren. Die Speicherverwaltung des Stapelspeichers ist so einfach, kostengünstig und einfach, dass Sie nicht möchten, dass die Speicherbereinigung beteiligt ist.

(Es gibt Systeme wie Smalltalk, bei denen Stapelrahmen erstklassige Objekte sind, die im Heap gespeichert sind, und Müll, der wie alle anderen Objekte gesammelt wird. Dies ist jedoch heutzutage nicht der beliebte Ansatz. Javas JVM und Die CLR von Microsoft verwendet den Hardware-Stack und den zusammenhängenden Speicher.)

63
Jeff Grigg

Drehen Sie Ihre Frage um. Die eigentliche motivierende Frage ist nter welchen Umständen können wir die Kosten für die Müllabfuhr vermeiden?

Zunächst einmal, wie hoch sind die Kosten für die Speicherbereinigung? Es gibt zwei Hauptkosten. Zuerst müssen Sie bestimmen, was lebt; das erfordert möglicherweise viel Arbeit. Zweitens müssen Sie die Löcher verdichten, die entstehen, wenn Sie etwas freigeben, das zwischen zwei noch lebenden Dingen aufgeteilt wurde. Diese Löcher sind verschwenderisch. Aber sie zu verdichten ist auch teuer.

Wie können wir diese Kosten vermeiden?

Wenn Sie ein Speichernutzungsmuster finden, in dem Sie niemals etwas Langlebiges, dann etwas Kurzlebiges und dann etwas Langlebiges zuweisen können Sie die Kosten für Löcher beseitigen. Wenn Sie garantieren können, dass für eine Teilmenge Ihres Speichers jede nachfolgende Zuordnung eine kürzere Lebensdauer hat als die vorherige in diesem Speicher, gibt es in diesem Speicher niemals Löcher.

Aber wenn wir das Lochproblem gelöst haben, haben wir auch das Garbage Collection-Problem gelöst . Haben Sie etwas in diesem Speicher, das noch lebt? Ja. Wurde alles zugeteilt, bevor es länger lebte? Ja - mit dieser Annahme haben wir die Möglichkeit von Löchern beseitigt. Daher müssen Sie nur sagen: "Lebt die letzte Zuordnung?" und Sie wissen, dass alles in diesem Speicher lebt.

Haben wir eine Reihe von Speicherzuordnungen, bei denen wir wissen, dass jede nachfolgende Zuweisung kürzer ist als die vorherige Zuweisung? Ja! Aktivierungsrahmen von Methoden werden immer in der entgegengesetzten Reihenfolge zerstört, in der sie erstellt wurden, da sie immer kürzer sind als die Aktivierung, die sie erstellt hat.

Daher können wir Aktivierungsrahmen auf dem Stapel speichern und wissen, dass sie niemals gesammelt werden müssen. Wenn sich ein Frame auf dem Stapel befindet, ist der gesamte Framesatz darunter längerlebig, sodass sie nicht gesammelt werden müssen. Und sie werden in der entgegengesetzten Reihenfolge zerstört, in der sie geschaffen wurden. Die Kosten für die Speicherbereinigung entfallen somit für Aktivierungsrahmen.

Aus diesem Grund haben wir den temporären Pool in erster Linie auf dem Stapel: weil dies eine einfache Möglichkeit ist, die Methodenaktivierung zu implementieren, ohne dass eine Speicherverwaltungsstrafe anfällt.

(Natürlich sind die Kosten für die Müllabfuhr des Speichers , auf den durch Verweise auf die Aktivierungsrahmen verwiesen wird, immer noch vorhanden.)

Betrachten Sie nun ein Kontrollflusssystem, in dem Aktivierungsrahmen nicht in einer vorhersehbaren Reihenfolge zerstört werden. Was passiert, wenn eine kurzlebige Aktivierung zu einer langlebigen Aktivierung führen kann? Wie Sie sich vorstellen können, können Sie in dieser Welt Sie können den Stapel nicht mehr verwenden, um die Notwendigkeit zum Sammeln von Aktivierungen zu optimieren. Die Aktivierungsmenge kann wieder Löcher enthalten.

C # 2.0 hat diese Funktion in Form von yield return. Eine Methode, die eine Rendite erzielt, wird zu einem späteren Zeitpunkt reaktiviert - das nächste Mal, wenn MoveNext aufgerufen wird - und wann dies geschieht, ist nicht vorhersehbar. Daher werden die Informationen, die sich normalerweise auf dem Stapel für den Aktivierungsrahmen des Iteratorblocks befinden, stattdessen auf dem Heap gespeichert, wo sie beim Sammeln des Enumerators durch Müll gesammelt werden.

In ähnlicher Weise können Sie mit der Funktion "async/await" in den nächsten Versionen von C # und VB) Methoden erstellen, deren Aktivierungen an genau definierten Punkten während der Aktion "ergeben" und "fortgesetzt" werden Da die Aktivierungsrahmen nicht mehr auf vorhersehbare Weise erstellt und zerstört werden, müssen alle Informationen, die früher im Stapel gespeichert waren, im Heap gespeichert werden.

Es ist nur ein Zufall der Geschichte, dass wir einige Jahrzehnte lang entschieden haben, dass Sprachen mit Aktivierungsrahmen, die streng geordnet erstellt und zerstört werden, in Mode sind. Da modernen Sprachen diese Eigenschaft zunehmend fehlt, erwarten Sie um immer mehr Sprachen zu sehen, die Fortsetzungen auf dem durch Müll gesammelten Haufen und nicht auf dem Stapel bestätigen.

19
Eric Lippert

Die naheliegendste und vielleicht nicht vollständigste Antwort ist, dass der Heap der Speicherort der Instanzdaten ist. Mit Instanzdaten sind die Daten gemeint, die die Instanzen von Klassen, auch Objekte genannt, darstellen, die zur Laufzeit erstellt werden. Diese Daten sind von Natur aus dynamisch und die Anzahl dieser Objekte und damit die Menge an Speicher, die sie belegen, ist nur zur Laufzeit bekannt. Es muss einige Schmerzen bei der Wiederherstellung dieses Speichers geben, oder lang laufende Programme würden im Laufe der Zeit den gesamten Speicher verbrauchen.

Es ist von Natur aus unwahrscheinlich, dass der Speicher, der von Klassendefinitionen, Konstanten und anderen statischen Datenstrukturen belegt wird, ungeprüft zunimmt. Da es im Speicher nur eine einzige Klassendefinition für eine unbekannte Anzahl von Laufzeitinstanzen dieser Klasse gibt, ist es sinnvoll, dass diese Art von Struktur keine Bedrohung für die Speichernutzung darstellt.

14
chad

Es lohnt sich, den Grund für die Speicherbereinigung zu berücksichtigen: Manchmal ist es schwierig zu wissen, wann Speicher freigegeben werden muss. Sie haben wirklich nur dieses Problem mit dem Haufen. Die auf dem Stapel zugewiesenen Daten werden irgendwann freigegeben, sodass dort keine Speicherbereinigung erforderlich ist. Es wird allgemein angenommen, dass die Dinge im Datenabschnitt für die Laufzeit des Programms zugeordnet sind.

10
Jason Baker
  1. Die Größe dieser ist vorhersehbar (konstant mit Ausnahme des Stapels, und der Stapel ist normalerweise auf einige MB begrenzt) und typischerweise sehr klein (zumindest im Vergleich zu den Hunderten von MB, die große Anwendungen möglicherweise zuweisen).

  2. Dynamisch zugewiesene Objekte haben normalerweise einen kleinen Zeitrahmen, in dem sie erreichbar sind. Danach können sie auf keinen Fall mehr referenziert werden. Vergleichen Sie dies mit Einträgen im Datenabschnitt, globalen Variablen usw.: Häufig gibt es einen Code, der direkt auf sie verweist (denken Sie an const char *foo() { return "foo"; }). Normalerweise ändert sich der Code nicht, sodass die Referenz erhalten bleibt und bei jedem Aufruf der Funktion eine weitere Referenz erstellt wird (soweit der Computer weiß, kann dies jederzeit erfolgen - es sei denn, Sie lösen das Stoppproblem ). Somit können Sie konnten sowieso den größten Teil dieses Speichers nicht freigeben, da er immer erreichbar wäre.

  3. In vielen von Müll gesammelten Sprachen wird alles, das zu dem ausgeführten Programm gehört, Heap-zugewiesen. In Python gibt es einfach keinen Datenabschnitt und keine vom Stapel zugewiesenen Werte (es gibt die Referenzen, die lokale Variablen sind, und es gibt den Aufrufstapel, aber auch keinen Wert im gleichen Sinne wie ein int in C). Jedes Objekt befindet sich auf dem Haufen.

3
user7043

Wie eine Reihe anderer Antwortender bereits gesagt haben, ist der Stapel Teil des Stammsatzes, sodass er nach Referenzen durchsucht, aber nicht per se "gesammelt" wird.

Ich möchte nur auf einige der Kommentare antworten, die implizieren, dass Müll auf dem Stapel keine Rolle spielt. Dies ist der Fall, da dadurch möglicherweise mehr Müll auf dem Heap als erreichbar angesehen wird. Conscientious VM und Compiler-Writer setzen tote Teile des Stacks entweder auf Null oder schließen sie auf andere Weise vom Scannen aus Ich weiß nicht, welche Technik derzeit bevorzugt wird.

Ein Begriff, der zur Beschreibung dieser besonderen Überlegung verwendet wird, ist platzsicher.

2
Ryan Culpepper

Lassen Sie mich auf einige grundlegende Missverständnisse hinweisen, die Sie und viele andere falsch verstanden haben:

"Warum fegt Garbage Collection nur den Haufen?" Es ist umgekehrt. Nur die einfachsten, konservativsten und langsamsten Müllsammler fegen den Haufen. Deshalb sind sie so langsam.

Schnelle Garbage Collectors durchsuchen nur den Stapel (und optional einige andere Roots, wie z. B. einige Globals für FFI-Zeiger und die Register für Live-Zeiger) und kopieren nur die Zeiger, die von den Stapelobjekten erreicht werden können. Der Rest wird weggeworfen (d. H. Ignoriert) und scannt überhaupt nicht auf dem Haufen.

Da der Heap etwa 1000x größer als der Stapel ist, ist ein solcher Stapelabtast-GC typischerweise viel schneller. ~ 15 ms vs 250 ms auf normal großen Haufen. Da die Objekte von einem Raum in einen anderen kopiert (verschoben) werden, wird sie meistens als Semi-Space-Kopiersammler bezeichnet. Sie benötigen 2x Speicher und können daher auf sehr kleinen Geräten wie Telefonen mit wenig Speicher meist nicht verwendet werden. Es ist komprimiert und daher im Gegensatz zu einfachen Mark & ​​Sweep-Heap-Scannern sehr cachefreundlich.

Da es sich um bewegliche Zeiger handelt, sind FFI, Identität und Referenzen schwierig. Identität wird normalerweise mit zufälligen IDs gelöst, Referenzen über Weiterleitungszeiger. FFI ist schwierig, da Fremdkörper keine Zeiger auf den alten Raum zurückhalten können. FFI-Zeiger werden normalerweise in einer separaten Haufenarena aufbewahrt, z. mit einem langsamen Mark & ​​Sweep, statischer Kollektor. Oder trivialer Malloc mit Nachzählung. Beachten Sie, dass Malloc einen enormen Overhead hat und noch mehr nachzählt.

Mark & ​​Sweep ist trivial zu implementieren, sollte jedoch nicht in realen Programmen verwendet und insbesondere nicht als Standardkollektor unterrichtet werden. Der bekannteste dieser schnellen Stapelabtast-Kopiersammler heißt Cheney-Zwei-Finger-Sammler .

1
rurban

Was ist auf dem Stapel zugeordnet? Lokale Variablen und Rücksprungadressen (in C). Wenn eine Funktion zurückkehrt, werden ihre lokalen Variablen verworfen. Es ist nicht notwendig, auch nicht schädlich, den Stapel zu fegen.

Viele dynamische Sprachen und auch Java oder C # sind in einer Systemprogrammiersprache implementiert, häufig in C. Man könnte sagen Java wird mit C-Funktionen und Verwendungen implementiert C lokale Variablen und daher muss der Garbage Collector von Java den Stapel nicht fegen.

Es gibt eine interessante Ausnahme: Der Garbage Collector von Chicken Scheme durchsucht den Stack (in gewisser Weise), da seine Implementierung den Stack zuerst als Garbage Collection verwendet. Generationsraum: siehe Chicken Scheme Design Wikipedia .

0
nalply