JavaScript-Speicherverwaltung





Guten Tag, Freunde!



In den allermeisten Fällen müssen wir uns als JavaScript-Entwickler keine Gedanken über die Arbeit mit dem Speicher machen. Der Motor erledigt das für uns.



Eines Tages werden Sie jedoch auf ein Problem namens "Speicherverlust" stoßen, das nur gelöst werden kann, wenn Sie wissen, wie Speicher in JavaScript zugewiesen wird.



In diesem Artikel werde ich erklären, wie Speicherzuweisung und Speicherbereinigung funktionieren und wie einige der häufig auftretenden Probleme im Zusammenhang mit Speicherlecks vermieden werden.



Speicherlebenszyklus



Wenn Sie eine Variable oder Funktion erstellen, weist die JavaScript-Engine Speicher dafür zu und gibt ihn frei, wenn er nicht mehr benötigt wird.



Beim Zuweisen von Speicher wird ein bestimmter Speicherplatz reserviert, und beim Freigeben von Speicher wird dieser Speicherplatz freigegeben, damit er für andere Zwecke verwendet werden kann.



Jedes Mal, wenn eine Variable oder Funktion erstellt wird, durchläuft der Speicher die folgenden Phasen:







  • Speicherzuweisung - Die Engine weist dem erstellten Objekt automatisch Speicher zu
  • Speichernutzung - Das Lesen und Schreiben von Daten in den Speicher ist nichts anderes als das Schreiben und Lesen von Daten aus einer Variablen
  • Speicher freigeben - Dieser Schritt wird auch automatisch von der Engine ausgeführt. Sobald der Speicher freigegeben ist, kann er für andere Zwecke verwendet werden.


Haufen und stapeln



Die nächste Frage ist, was Erinnerung bedeutet. Wo werden die Daten tatsächlich gespeichert?



Der Motor hat zwei solche Stellen: den Haufen und den Stapel. Heap und Stack sind Datenstrukturen, die von der Engine für verschiedene Zwecke verwendet werden.



Stapel: statische Speicherzuordnung







Alle Daten im Beispiel werden auf dem Stapel gespeichert, da es sich um ein Grundelement handelt.



Ein Stapel ist eine Datenstruktur, die zum Speichern statischer Daten verwendet wird. Statische Daten sind Daten, deren Größe der Engine in der Kompilierungsphase des Codes bekannt ist. In JavaScript sind solche Daten Grundelemente (Zeichenfolgen, Zahlen, Boolesche Werte, undefiniert und null) und Verweise, die auf Objekte und Funktionen verweisen.



Da die Engine weiß, dass sich die Größe der Daten nicht ändert, weist sie jedem Wert eine feste Speichergröße zu. Das Zuweisen von Speicher vor dem Ausführen Ihres Codes wird als statische Speicherzuweisung bezeichnet. Da die Engine eine feste Speichergröße zuweist, gibt es bestimmte Grenzen für diese Größe, die stark vom Browser abhängig sind.



Heap: dynamische Speicherzuordnung



Der Heap dient zum Speichern von Objekten und Funktionen. Im Gegensatz zum Stapel weist die Engine Objekten keine feste Speichergröße zu. Der Speicher wird nach Bedarf zugewiesen. Diese Speicherzuordnung wird als dynamisch bezeichnet. Hier ist eine kleine Vergleichstabelle:



Stapel Haufen
Primitive Werte und Referenzen Objekte und Funktionen
Die Größe ist zur Kompilierungszeit bekannt Die Größe ist zur Laufzeit bekannt
Feste Speicherzuweisung Die Speichergröße für jedes Objekt ist nicht begrenzt


Beispiele von



Schauen wir uns einige Beispiele an.



  const person = {
    name: "John",
    age: 24,
  };


Die Engine reserviert Speicher für dieses Objekt auf dem Heap. Eigenschaftswerte werden jedoch auf dem Stapel gespeichert.



  const hobbies = ["hiking", "reading"];


Arrays sind Objekte und werden daher auf dem Heap gespeichert



  let name = "John";
  const age = 24;

  name = "John Doe";
  const firstName = name.slice(0, 4);


Primitive sind unveränderlich. Dies bedeutet, dass JavaScript anstelle der Änderung des ursprünglichen Werts einen neuen Wert erstellt.



Links



Alle Variablen werden auf dem Stapel gespeichert. Bei nicht primitiven Werten speichert der Stapel Verweise auf ein Objekt auf dem Heap. Der Speicher auf dem Heap ist ungeordnet. Deshalb brauchen wir Links auf dem Stapel. Sie können sich Links als Adressen und Objekte als Häuser an einer bestimmten Adresse vorstellen.







Im obigen Bild sehen wir, wie die verschiedenen Werte gespeichert werden. Beachten Sie, dass person und newPerson auf dasselbe Objekt verweisen



Beispiele von



  const person = {
    name: "John",
    age: 24,
  };


Dadurch wird ein neues Objekt auf dem Heap und ein Verweis darauf auf dem Stapel erstellt



Müllabfuhr



Sobald die Engine feststellt, dass eine Variable oder Funktion nicht mehr verwendet wird, gibt sie den von ihr belegten Speicher frei.



Tatsächlich ist das Problem der Freigabe von nicht verwendetem Speicher nicht lösbar: Es gibt keinen perfekten Algorithmus, um ihn zu lösen.



In diesem Artikel werden zwei Algorithmen vorgestellt, die die bisher besten Lösungen bieten: Referenzzählung, Speicherbereinigung und Markieren und Durchsuchen.



Speicherbereinigung durch Referenzzählung



Hier ist alles einfach - Objekte, auf die keine Referenzpunkte aus dem Speicher gelöscht werden. Schauen wir uns ein Beispiel an. Linien stehen für Links.







Beachten Sie, dass nur das Objekt "Hobbys" auf dem Heap verbleibt, da nur dieses Objekt auf dem Stapel referenziert wird.



Zyklische Links



Das Problem bei dieser Speicherbereinigungsmethode ist die Unfähigkeit, Zirkelverweise zu definieren. Dies ist eine Situation, in der zwei oder mehr Objekte aufeinander zeigen, aber keine XRefs haben. Jene. Auf diese Objekte kann von außen nicht zugegriffen werden.



  const son = {
    name: "John",
  };

  const dad = {
    name: "Johnson",
  };

  son.dad = dad;
  dad.son = son;

  son = null;
  dad = null;






Da sich die Objekte "Sohn" und "Vater" aufeinander beziehen, kann der Referenzzählalgorithmus keinen Speicher freigeben. Diese Objekte stehen jedoch nicht mehr für externen Code zur Verfügung.



Algorithmus zum Markieren und Reinigen



Dieser Algorithmus löst das Problem der Zirkelverweise. Anstatt die Referenzen zu zählen, die auf ein Objekt verweisen, wird die Zugänglichkeit des Objekts vom Stammobjekt aus bestimmt. Das Stammobjekt ist das "Fenster" -Objekt im Browser oder das "globale" Objekt in Node.js.







Der Algorithmus markiert Objekte als nicht erreichbar und entfernt sie. Zirkelverweise sind somit kein Problem mehr. Im obigen Beispiel sind die Objekte "Vater" und "Sohn" vom Stammobjekt aus nicht erreichbar. Sie werden als Papierkorb markiert und entfernt. Der betreffende Algorithmus ist seit 2012 in allen modernen Browsern implementiert. Die seitdem vorgenommenen Verbesserungen betreffen Implementierungs- und Leistungsverbesserungen, jedoch nicht die Kernidee des Algorithmus.



Kompromisse



Durch die automatische Speicherbereinigung können wir uns auf das Erstellen von Anwendungen konzentrieren und keine Zeit mit der Speicherverwaltung verschwenden. Alles hat jedoch seinen Preis.



Speichernutzung



Da Algorithmen einige Zeit benötigen, um festzustellen, dass kein Speicher mehr verwendet wird, verwenden JavaScript-Anwendungen in der Regel mehr Speicher, als sie tatsächlich benötigen.



Obwohl die Objekte als Müll markiert sind, muss der Kollektor entscheiden, wann sie gesammelt werden sollen, um den Programmablauf nicht zu blockieren. Wenn Sie möchten, dass Ihre Anwendung hinsichtlich der Speichernutzung so effizient wie möglich ist, sollten Sie eine niedrigere Programmiersprache verwenden. Aber denken Sie daran, dass solche Sprachen ihre eigenen Kompromisse haben.



Performance



Garbage Collection-Algorithmen werden regelmäßig ausgeführt, um nicht verwendete Objekte zu bereinigen. Das Problem ist, dass wir als Entwickler nicht genau wissen, wann dies geschehen wird. Große Mengen an Speicherbereinigung oder häufige Speicherbereinigung können die Leistung beeinträchtigen, da eine bestimmte Menge an Verarbeitungsleistung erforderlich ist. Dies geschieht jedoch normalerweise unbemerkt von Benutzer und Entwickler.



Speicherlecks



Lassen Sie uns einen kurzen Blick auf die häufigsten Probleme mit Speicherverlusten werfen.



Globale Variablen



Wenn Sie eine Variable deklarieren, ohne eines der Schlüsselwörter (var, let oder const) zu verwenden, wird die Variable zu einer Eigenschaft des globalen Objekts.



  users = getUsers();


Wenn Sie Ihren Code im strengen Modus ausführen, wird dies vermieden.



Manchmal deklarieren wir absichtlich globale Variablen. In diesem Fall müssen Sie den Wert "null" zuweisen, um den von einer solchen Variablen belegten Speicher freizugeben:



  window.users = null;


Vergessene Timer und Rückrufe



Wenn Sie Timer und Rückrufe vergessen, kann sich die Speichernutzung Ihrer Anwendung dramatisch erhöhen. Seien Sie vorsichtig, insbesondere beim Erstellen von Single-Page-Anwendungen (SPA), bei denen Ereignishandler und Rückrufe dynamisch hinzugefügt werden.



Vergessene Timer



  const object = {};
  const intervalId = setInterval(function () {
    // ,   ,      ,
    //   ,     
    doSomething(object);
  }, 2000);


Der obige Code führt die Funktion alle 2 Sekunden aus. Wenn Sie den Timer nicht mehr benötigen, müssen Sie ihn abbrechen durch:



  clearInterval(intervalId);


Dies ist besonders wichtig für SPA. Selbst wenn Sie zu einer anderen Seite wechseln, auf der der Timer nicht verwendet wird, wird er im Hintergrund ausgeführt.



Vergessene Rückrufe



Angenommen, Sie registrieren einen Handler für einen Schaltflächenklick, den Sie später löschen. Dies ist zwar kein Problem mehr, es wird jedoch empfohlen, nicht mehr benötigte Handler zu entfernen:



  const element = document.getElementById("button");
  const onClick = () => alert("hi");

  element.addEventListener("click", onClick);

  element.removeEventListener("click", onClick);
  element.parentNode.removeChild(element);


Links außerhalb des DOM



Dieser Speicherverlust ähnelt den vorherigen. Er tritt beim Speichern von DOM-Elementen in JavaScript auf:



  const elements = [];
  const element = document.getElementById("button");
  elements.push(element);

  function removeAllElements() {
    elements.forEach((item) => {
      document.body.removeChild(document.getElementById(item.id));
    });
  }


Wenn Sie eines dieser Elemente entfernen, sollten Sie es auch aus dem Array entfernen. Andernfalls können solche Elemente vom Garbage Collector nicht entfernt werden:



  const elements = [];
  const element = document.getElementById("button");
  elements.push(element);

  function removeAllElements() {
    elements.forEach((item, index) => {
      document.body.removeChild(document.getElementById(item.id));
      elements.splice(index, 1);
    });
  }


Ich hoffe, Sie haben etwas Interessantes für sich gefunden. Vielen Dank für Ihre Aufmerksamkeit.



All Articles