Grundlagen der JavaScript-Speicherverwaltung: Wie es funktioniert und welche Probleme auftreten können





Die meisten Entwickler denken selten darüber nach, wie die JavaScript-Speicherverwaltung implementiert ist. Die Engine erledigt normalerweise alles für den Programmierer, daher macht es für diesen keinen Sinn, über die Prinzipien des Speicherverwaltungsmechanismus nachzudenken.



Früher oder später müssen sich Entwickler jedoch immer noch mit Speicherproblemen wie Lecks befassen. Nun, es wird nur möglich sein, mit ihnen umzugehen, wenn der Speicherzuweisungsmechanismus verstanden ist. Dieser Artikel ist Erklärungen gewidmet. Es enthält auch Tipps zu den häufigsten Arten von Speicherverlusten in JavaScript.



Speicherlebenszyklus



Beim Erstellen von Funktionen, Variablen usw. In JavaScript weist die Engine eine bestimmte Menge an Speicher zu. Dann gibt er es frei - nachdem der Speicher nicht mehr benötigt wird.



Tatsächlich kann die Speicherzuweisung als der Vorgang des Reservierens einer bestimmten Speichermenge bezeichnet werden. Nun, seine Freigabe ist die Rückgabe der Reserve an das System. Sie können es beliebig oft wiederverwenden.



Wenn eine Variable deklariert oder eine Funktion erstellt wird, durchläuft der Speicher die nächste Schleife.







Hier in Blöcken:



  • Zuweisen ist die Speicherzuordnung, die die Engine ausführt. Es ordnet den Speicher zu, der für das erstellte Objekt erforderlich ist.
  • Verwendung - Speichernutzung. Der Entwickler ist für diesen Moment verantwortlich und schreibt den Code zum Lesen und Schreiben in den Speicher ein.
  • Freigeben - Speicher freigeben. Hier kommt JavaScript wieder zur Geltung. Nachdem die Reserve freigegeben wurde, kann der Speicher auch für andere Zwecke verwendet werden.


"Objekte" im Kontext der Speicherverwaltung sind nicht nur JS-Objekte, sondern auch Funktionen und Bereiche.



Speicherstapel und Heap



Im Allgemeinen scheint alles klar zu sein - JavaScript weist Speicher für alles zu, was der Entwickler im Code angibt, und wenn alle Vorgänge abgeschlossen sind, wird der Speicher freigegeben. Aber wo werden die Daten gespeichert?



Es gibt zwei Optionen - auf dem Speicherstapel und auf dem Heap. Was ist das Erste, was ist das Zweite - der Name der Datenstrukturen, die von der Engine für verschiedene Zwecke verwendet werden.



Stapel ist statische Speicherzuordnung







Die Definition des Stapels ist vielen bekannt. Es ist eine Datenstruktur, die zum Speichern statischer Daten verwendet wird. Ihre Größe ist zur Kompilierungszeit immer bekannt. JS hat primitive Werte wie Zeichenfolge, Zahl, Boolescher Wert, undefiniert und Null sowie Funktions- und Objektreferenzen enthalten.



Die Engine "versteht", dass sich die Größe der Daten nicht ändert, und weist daher jedem der Werte eine feste Speichermenge zu. Das Zuweisen von Speicher vor der Ausführung wird als statische Speicherzuweisung bezeichnet.



Da der Browser jedem Datentyp im Voraus Speicher zuweist, ist die Größe der Daten, die dort abgelegt werden können, begrenzt. Da der Browser jedem Datentyp im Voraus Speicher zuweist, ist die Größe der Daten, die dort abgelegt werden können, begrenzt.



Der Heap - dynamische Speicherzuordnung Der Heap



ist vielen so vertraut wie der Stapel. Es wird zum Speichern von Objekten und Funktionen verwendet.



Im Gegensatz zum Stapel kann die Engine jedoch nicht "wissen", wie viel Speicher für ein bestimmtes Objekt benötigt wird, sodass der Speicher nach Bedarf zugewiesen wird. Und diese Art der Speicherzuweisung wird als "dynamisch" (dynamische Speicherzuweisung) bezeichnet.



Einige Beispiele



Die Kommentare zum Code geben die Nuancen der Speicherzuweisung an.



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





// JavaScript reserviert Speicher für dieses Objekt auf dem Heap.

// Die Werte selbst sind primitiv und werden daher auf dem Stapel gespeichert.



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





// Arrays sind ebenfalls Objekte, daher werden sie auf den Heap verschoben.



let name = 'John'; // Speicher für den String zuweisen

const age = 24; // Speicher für den Nummernnamen zuweisen

= 'John Doe'; // Speicher für eine neue Zeile

zuweisen const firstName = name.slice (0,4); // Speicher für eine neue Zeile zuweisen



// Primitive Werte sind von Natur aus unveränderlich: Anstatt den Anfangswert zu ändern,

// erstellt JavaScript einen anderen.



JavaScript-Links



Was den Stapel betrifft, zeigen alle Variablen darauf. Wenn der Wert nicht primitiv ist, enthält der Stapel einen Verweis auf das Heap-Objekt.



Es gibt keine bestimmte Reihenfolge, was bedeutet, dass eine Referenz auf den gewünschten Speicherbereich auf dem Stapel gespeichert ist. In einer solchen Situation sieht das Objekt im Heap wie ein Gebäude aus, aber der Link ist seine Adresse.



JS speichert Objekte und Funktionen auf dem Heap. Primitive Werte und Referenzen befinden sich jedoch auf dem Stapel.







Dieses Bild zeigt die Speicherorganisation verschiedener Werte. Beachten Sie, dass person und newPerson hier auf dasselbe Objekt verweisen.



Beispiel



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





// Auf dem Heap wird ein neues Objekt erstellt und auf dem Stapel ein Verweis darauf.



Im Allgemeinen sind Links in JavaScript äußerst wichtig.



Müllabfuhr



Jetzt ist die Zeit gekommen, zum Lebenszyklus des Gedächtnisses zurückzukehren, nämlich seiner Freisetzung.



Die JavaScript-Engine ist nicht nur für die Speicherzuweisung verantwortlich, sondern auch für die Freigabe. In diesem Fall gibt der Garbage Collector Speicher an das System zurück.



Und sobald die Engine "sieht", dass die Variable oder Funktion nicht mehr benötigt wird, wird der Speicher freigegeben.



Aber hier gibt es ein Schlüsselproblem. Tatsache ist, dass es unmöglich ist zu entscheiden, ob ein bestimmter Speicherbereich benötigt wird oder nicht. Es gibt keine Algorithmen, die so präzise sind, dass Speicher in Echtzeit frei ist.



Es stimmt, es gibt nur gut funktionierende Algorithmen, mit denen Sie dies tun können. Sie sind nicht perfekt, aber immer noch viel besser als viele andere. Unten - eine Geschichte über die Garbage Collection, die auf Referenzzählung basiert, sowie über den "Flagging-Algorithmus".



Was ist mit Links?



Dies ist ein sehr einfacher Algorithmus. Es werden die Objekte entfernt, auf die keine anderen Referenzpunkte verweisen. Hier ist ein Beispiel , das es ziemlich gut erklärt.



Wenn Sie das Video gesehen haben, haben Sie wahrscheinlich bemerkt, dass Hobbys das einzige Objekt im Heap sind, auf das auf dem Stapel verwiesen wurde.



Fahrräder



Der Nachteil des Algorithmus ist, dass er Zirkelverweise nicht berücksichtigen kann. Sie treten auf, wenn ein oder mehrere Objekte aus Sicht des Codes außerhalb der Reichweite aufeinander verweisen.



let son = {
  name: 'John',
};
let dad = {
  name: 'Johnson',
}
 
son.dad = dad;
dad.son = son;
son = null;
dad = null;
      
      







Hier beziehen sich Sohn und Vater aufeinander. Es gibt lange Zeit keinen Zugriff auf Objekte, aber der Algorithmus gibt den ihnen zugewiesenen Speicher nicht frei.



Gerade weil der Algorithmus Referenzen zählt, bewirkt das Zuweisen von Null zu Objekten nichts, da jedes Objekt noch eine Referenz hat.



Algorithmus für Anmerkungen



Hier kommt ein anderer Algorithmus zur Rettung, der als Mark and Sweep-Methode bezeichnet wird. Dieser Algorithmus zählt keine Referenzen, bestimmt jedoch, ob über das Stammobjekt auf verschiedene Objekte zugegriffen werden kann. Im Browser ist dies ein Fenster und in Node.js ist es global.







Wenn das Objekt nicht verfügbar ist, markiert der Algorithmus es und entfernt es dann. In diesem Fall werden die Stammobjekte niemals zerstört. Das Problem der zyklischen Referenzen ist hier nicht relevant - der Algorithmus ermöglicht es uns zu verstehen, dass weder Vater noch Sohn bereits unzugänglich sind, sodass sie "weggefegt" und der Speicher an das System zurückgegeben werden können.



Seit 2012 sind absolut alle Browser mit Garbage Collectors ausgestattet, die genau nach der Markierungs- und Sweep-Methode arbeiten.



Nicht ohne Nachteile hier.





Man würde denken, dass alles in Ordnung ist, und jetzt können Sie die Speicherverwaltung vergessen und alles dem Algorithmus überlassen. Das ist aber nicht so.



Große Speichernutzung



Da Algorithmen nicht wissen, wann kein Speicher mehr benötigt wird, können JavaScript-Anwendungen mehr Speicher verwenden, als sie benötigen. Und nur der Kollektor kann entscheiden, ob der zugewiesene Speicher freigegeben werden soll oder nicht.



JavaScript ist besser in der Verwaltung des Speichers in einfachen Sprachen. Hier gibt es aber auch Nachteile, die berücksichtigt werden müssen. Insbesondere bietet JS im Gegensatz zu einfachen Sprachen, in denen der Programmierer "manuell" die Speicherzuweisung und -freigabe übernimmt, keine Speicherverwaltungstools an.



Performance



Der Speicher wird nicht in jedem neuen Moment gelöscht. Die Freigabe erfolgt in regelmäßigen Abständen. Entwickler können jedoch nicht genau wissen, wann diese Prozesse gestartet werden.



In einigen Fällen kann sich die Speicherbereinigung daher negativ auf die Leistung auswirken, da der Algorithmus bestimmte Ressourcen benötigt, um zu funktionieren. Die Situation wird zwar selten völlig unüberschaubar. Meistens sind die Folgen davon mikroskopisch.



Speicherlecks



Speicherlecks sind eines der frustrierendsten Dinge in der Entwicklung. Wenn Sie jedoch die häufigsten Arten von Lecks kennen, können Sie das Problem ohne große Schwierigkeiten umgehen.



Globale Variablen



Speicherlecks treten am häufigsten aufgrund der Speicherung von Daten in globalen Variablen auf.



Wenn Sie im Browser einen Fehler machen und var anstelle von const oder let verwenden, hängt die Engine die Variable an das Fensterobjekt an. In ähnlicher Weise wird die Operation für Funktionen ausgeführt, die durch die Wortfunktion definiert sind.



user = getUser();
var secondUser = getUser();
function getUser() {
  return 'user';
}
      
      





// Alle drei Variablen - user, secondUser und

// getUser - werden an das Fensterobjekt angehängt.



Dies ist nur mit Funktionen und Variablen möglich, die im globalen Bereich deklariert sind. Sie können dieses Problem umgehen, indem Sie Ihren Code im strengen Modus ausführen.



Globale Variablen werden oft absichtlich deklariert, dies ist nicht immer ein Fehler. ABER auf keinen Fall dürfen wir vergessen, Speicher freizugeben, nachdem die Daten nicht mehr benötigt werden. Dazu müssen Sie der globalen Variablen null zuweisen.



window.users = null;



Rückrufe und Timer



Die Anwendung benötigt mehr Speicher als sie sollte, auch wenn wir Timer und Rückrufe vergessen. Das Hauptproblem sind Single-Page-Anwendungen (SPA) sowie das dynamische Hinzufügen von Rückrufen und Ereignishandlern.



Vergessene Timer



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





Diese Funktion wird alle zwei Sekunden ausgeführt. Die Umsetzung sollte nicht endlos sein. Das Problem ist, dass Objekte, die eine Referenz im Intervall haben, erst zerstört werden, wenn das Intervall gelöscht ist. Daher müssen Sie rechtzeitig verschreiben:

clearInterval (intervalId);



Vergessener Rückruf Ein



Problem kann auftreten, wenn ein onClick-Handler an eine Schaltfläche angehängt wird und die Schaltfläche selbst danach entfernt wird - beispielsweise wird sie nicht mehr benötigt.



Bisher konnten die meisten Browser den für einen solchen Ereignishandler zugewiesenen Speicher einfach nicht freigeben. Jetzt gehört dieses Problem der Vergangenheit an, aber es lohnt sich nicht, die Handler zu belassen, die Sie nicht mehr benötigen.



const element = document.getElementById('button');
const onClick = () => alert('hi');
element.addEventListener('click', onClick);
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
      
      







Vergessene DOM-Elemente in Variablen



Dies ähnelt dem vorherigen Fall. Der Fehler tritt auf, wenn DOM-Elemente in einer Variablen gespeichert werden.



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 der oben genannten Elemente entfernen, sollten Sie auch darauf achten, es aus dem Array zu entfernen. Andernfalls wird es vom Garbage Collector nicht automatisch entfernt.



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);
 });
}
      
      





Durch Entfernen eines Elements aus dem Array aktualisieren Sie dessen Inhalt mit der Liste der Elemente auf der Seite.



Da jedes Hauselement einen Verweis auf sein übergeordnetes Element hat, verhindert dies referenziell, dass der Garbage Collector den vom übergeordneten Element belegten Speicher freigibt, was zu Lecks führt.



Im trockenen Rückstand



Der Artikel beschreibt die allgemeinen Mechanismen der Speicherzuweisung und der Autor zeigte, welche Probleme auftreten können und wie man damit umgeht. All dies ist für jeden Java Script-Entwickler wichtig.



, Frontend- Skillbox:



, - ( SPA — Single Page Applications).



, “ ” — ( , ), , , .



— . .



, ( , , ). , , “” — garbage collector.



- (js , garbage collector’a). , .



All Articles