Die Speicherverwaltung in JavaScript erfolgt automatisch und für uns unsichtbar. Wir erstellen Grundelemente, Objekte, Funktionen ... Alles, was Speicher erfordert.
Was passiert, wenn etwas nicht mehr benötigt wird? Wie erkennt und bereinigt die JavaScript-Engine es?
Das Hauptkonzept der Speicherverwaltung in JavaScript ist die Erreichbarkeit .
Einfach ausgedrückt sind „erreichbare“ Werte diejenigen, die auf irgendeine Weise zugänglich oder nutzbar sind. Sie werden garantiert im Speicher gespeichert.
Es gibt einen Basissatz von inhärent erreichbaren Werten, der aus offensichtlichen Gründen nicht gelöscht werden kann.
Zum Beispiel:
Diese Werte werden Wurzeln genannt.
Die aktuell ausgeführte Funktion, ihre lokalen Variablen und Parameter.
Andere Funktionen in der aktuellen Kette verschachtelter Aufrufe, ihre lokalen Variablen und Parameter.
Globale Variablen.
(Es gibt auch einige andere, interne)
Jeder andere Wert gilt als erreichbar, wenn er von einem Stamm aus über eine Referenz oder eine Referenzkette erreichbar ist.
Wenn es beispielsweise ein Objekt in einer globalen Variablen gibt und dieses Objekt über eine Eigenschaft verfügt, die auf ein anderes Objekt verweist, gilt dieses Objekt als erreichbar. Und diejenigen, auf die es verweist, sind auch erreichbar. Detaillierte Beispiele folgen.
In der JavaScript-Engine gibt es einen Hintergrundprozess namens Garbage Collector. Es überwacht alle Objekte und entfernt diejenigen, die nicht mehr erreichbar sind.
Hier ist das einfachste Beispiel:
// Benutzer hat eine Referenz auf das Objekt let user = { Name: „John“ };
Hier stellt der Pfeil eine Objektreferenz dar. Die globale Variable "user"
verweist auf das Objekt {name: "John"}
(wir nennen es der Kürze halber John). Die "name"
-Eigenschaft von John speichert ein Grundelement, sodass es innerhalb des Objekts gezeichnet wird.
Wenn der Wert von user
überschrieben wird, geht die Referenz verloren:
Benutzer = null;
Jetzt wird John unerreichbar. Es gibt keine Möglichkeit, darauf zuzugreifen, keine Verweise darauf. Der Garbage Collector verschrottet die Daten und gibt den Speicher frei.
Stellen wir uns nun vor, wir hätten die Referenz vom user
zum admin
kopiert:
// Benutzer hat eine Referenz auf das Objekt let user = { Name: „John“ }; let admin = user;
Wenn wir nun dasselbe tun:
Benutzer = null;
…Dann ist das Objekt weiterhin über die globale admin
Variable erreichbar und muss daher im Speicher bleiben. Wenn wir auch admin
überschreiben, kann es entfernt werden.
Nun ein komplexeres Beispiel. Die Familie:
Funktion heiraten (Mann, Frau) { Frau.Ehemann = Mann; Mann.Frau = Frau; zurückkehren { Vater: Mann, Mutter: Frau } } lass Familie = heiraten({ Name: „John“ }, { Name: „Ann“ });
Die Funktion marry
„verheiratet“ zwei Objekte, indem sie ihnen Referenzen zueinander gibt und ein neues Objekt zurückgibt, das beide enthält.
Die resultierende Speicherstruktur:
Ab sofort sind alle Objekte erreichbar.
Entfernen wir nun zwei Referenzen:
Familie.Vater löschen; Familie.Mutter.Ehemann löschen;
Es reicht nicht aus, nur eine dieser beiden Referenzen zu löschen, da alle Objekte weiterhin erreichbar wären.
Wenn wir jedoch beide löschen, können wir sehen, dass John keine eingehende Referenz mehr hat:
Ausgehende Referenzen spielen keine Rolle. Nur eingehende können ein Objekt erreichbar machen. Daher ist John nun nicht mehr erreichbar und wird mit all seinen Daten, auf die ebenfalls nicht mehr zugegriffen werden konnte, aus dem Speicher entfernt.
Nach der Müllabfuhr:
Es ist möglich, dass die gesamte Insel der miteinander verbundenen Objekte unerreichbar wird und aus dem Speicher entfernt wird.
Das Quellobjekt ist das gleiche wie oben. Dann:
Familie = null;
Das In-Memory-Bild wird zu:
Dieses Beispiel zeigt, wie wichtig das Konzept der Erreichbarkeit ist.
Es ist offensichtlich, dass John und Ann immer noch miteinander verbunden sind, beide haben eingehende Referenzen. Aber das reicht nicht.
Die Verknüpfung des ehemaligen "family"
-Objekts mit der Wurzel wurde aufgehoben, es gibt keinen Verweis mehr darauf, sodass die gesamte Insel nicht mehr erreichbar ist und entfernt wird.
Der grundlegende Garbage-Collection-Algorithmus heißt „Mark-and-Sweep“.
Die folgenden „Garbage Collection“-Schritte werden regelmäßig durchgeführt:
Der Garbage Collector nimmt Wurzeln und „markiert“ (erinnert) sie.
Dann besucht und „markiert“ es alle Referenzen von ihnen.
Anschließend besucht es markierte Objekte und markiert deren Referenzen. Alle besuchten Objekte werden gespeichert, um in Zukunft nicht dasselbe Objekt zweimal zu besuchen.
… Und so weiter, bis alle (von den Wurzeln aus) erreichbaren Referenzen besucht sind.
Alle Objekte außer den markierten werden entfernt.
Lassen Sie unsere Objektstruktur beispielsweise so aussehen:
Wir können auf der rechten Seite deutlich eine „unerreichbare Insel“ erkennen. Sehen wir uns nun an, wie der „Mark-and-Sweep“-Garbage Collector damit umgeht.
Der erste Schritt markiert die Wurzeln:
Dann folgen wir ihren Referenzen und markieren referenzierte Objekte:
…Und folgen Sie weiterhin weiteren Referenzen, soweit möglich:
Nun gelten die Objekte, die dabei nicht besucht werden konnten, als nicht erreichbar und werden entfernt:
Wir können uns den Prozess auch so vorstellen, als würde ein riesiger Eimer Farbe aus den Wurzeln verschüttet, der alle Bezüge durchfließt und alle erreichbaren Objekte markiert. Die nicht markierten werden dann entfernt.
Das ist das Konzept, wie Garbage Collection funktioniert. JavaScript-Engines wenden zahlreiche Optimierungen an, um die Ausführung zu beschleunigen und keine Verzögerungen bei der Codeausführung zu verursachen.
Einige der Optimierungen:
Generationensammlung – Objekte werden in zwei Gruppen aufgeteilt: „Neue“ und „Alte“. In typischem Code haben viele Objekte eine kurze Lebensdauer: Sie erscheinen, erledigen ihre Arbeit und sterben schnell. Daher ist es sinnvoll, neue Objekte zu verfolgen und in diesem Fall den Speicher von ihnen zu löschen. Wer lange genug überlebt, wird „alt“ und wird seltener untersucht.
Inkrementelle Sammlung – wenn viele Objekte vorhanden sind und wir versuchen, den gesamten Objektsatz auf einmal zu durchlaufen und zu markieren, kann dies einige Zeit dauern und zu sichtbaren Verzögerungen bei der Ausführung führen. Daher teilt die Engine den gesamten Satz vorhandener Objekte in mehrere Teile auf. Und dann diese Teile nacheinander räumen. Es gibt viele kleine Müllsammlungen statt einer ganzen. Das erfordert etwas zusätzliche Buchhaltung zwischen ihnen, um Änderungen nachzuverfolgen, aber wir bekommen viele kleine Verzögerungen statt einer großen.
Leerlaufzeiterfassung – der Garbage Collector versucht, nur dann zu laufen, wenn die CPU im Leerlauf ist, um mögliche Auswirkungen auf die Ausführung zu reduzieren.
Es gibt andere Optimierungen und Varianten von Garbage-Collection-Algorithmen. So sehr ich sie hier auch beschreiben möchte, ich muss mich zurückhalten, da verschiedene Engines unterschiedliche Optimierungen und Techniken implementieren. Und was noch wichtiger ist: Die Dinge ändern sich mit der Weiterentwicklung der Motoren, sodass es sich wahrscheinlich nicht lohnt, sich „im Voraus“ tiefer zu informieren, ohne dass ein wirklicher Bedarf besteht. Sofern es sich natürlich nicht um reines Interesse handelt, finden Sie unten einige Links für Sie.
Das Wichtigste, was Sie wissen sollten:
Die Garbage Collection wird automatisch durchgeführt. Wir können es nicht erzwingen oder verhindern.
Objekte bleiben im Speicher erhalten, solange sie erreichbar sind.
Referenziert zu sein ist nicht dasselbe wie erreichbar zu sein (von einem Root aus): Eine Gruppe miteinander verbundener Objekte kann als Ganzes unerreichbar werden, wie wir im obigen Beispiel gesehen haben.
Moderne Motoren implementieren fortschrittliche Garbage-Collection-Algorithmen.
Ein allgemeines Buch „The Garbage Collection Handbook: The Art of Automatic Memory Management“ (R. Jones et al.) behandelt einige davon.
Wenn Sie mit Low-Level-Programmierung vertraut sind, finden Sie detailliertere Informationen zum Garbage Collector von V8 im Artikel Eine Tour durch V8: Garbage Collection.
Der V8-Blog veröffentlicht von Zeit zu Zeit auch Artikel über Änderungen in der Speicherverwaltung. Um mehr über die Speicherbereinigung zu erfahren, sollten Sie sich natürlich besser darauf vorbereiten, sich über V8-Interna im Allgemeinen zu informieren und den Blog von Vyacheslav Egorov zu lesen, der als einer der V8-Ingenieure arbeitete. Ich sage: „V8“, weil es am besten in Artikeln im Internet behandelt wird. Bei anderen Engines sind viele Ansätze ähnlich, die Garbage Collection unterscheidet sich jedoch in vielen Aspekten.
Wenn Sie Optimierungen auf niedriger Ebene benötigen, sind fundierte Kenntnisse über Motoren von Vorteil. Es wäre ratsam, dies als nächsten Schritt einzuplanen, nachdem Sie mit der Sprache vertraut sind.