
Informieren Sie sich über Speicherlecks in JavaScript und was Sie tun können, um sie zu beheben!
In diesem Artikel untersuchen wir Typologien von Speicherlecks in clientseitigem JavaScript-Code. Wir lernen auch, wie Sie die Entwicklertools von Chrome verwenden, um sie zu finden. Lasst uns beginnen!
Einführung
Speicherlecks sind ein Problem, mit dem alle Entwickler konfrontiert sind, selbst wenn sie mit Sprachen arbeiten, die Speicher verwalten, gibt es Fälle, in denen Speicherlecks auftreten. Lecks sind die Ursache für eine ganze Reihe von Problemen: Verlangsamung, Abstürze, hohe Reaktionszeiten und sogar Probleme mit anderen Anwendungen.
Was ist ein Speicherleck?
Ein Speicherleck kann im Wesentlichen als ein Speicherbereich definiert werden, der von einer Anwendung nicht mehr verwendet, aber nicht freigegeben und daher dem Betriebssystem nicht zur Verfügung gestellt wird. Sprachen haben unterschiedliche Methoden zur Verwaltung des Gedächtnisses. Diese Methoden verringern die Wahrscheinlichkeit, dass ein Speicherverlust auftritt. Zu wissen, ob ein Speicherbereich unbrauchbar geworden ist oder nicht, ist jedoch ein potentiell unlösbares Problem. Nur Designer können klären, ob ein Speicherbereich ungenutzt ist. Wikipedia hat gute Artikel zur manuellen und automatischen Speicherverwaltung.
Speicherverwaltung in JavaScript
JavaScript ist eine der Sprachen mit Garbage Collection. Garbage-Collection-Sprachen helfen Entwicklern bei der Speicherverwaltung, indem sie regelmäßig prüfen, welcher zuvor zugewiesene Speicherbereich für andere Teile der Anwendung zugänglich bleiben soll. Mit anderen Worten, Garbage-Collection-basierte Sprachen reduzieren das Problem der Speicherverwaltung von „welcher Speicherbereich wird noch benötigt“ auf „welcher Speicherbereich bleibt für den Rest der Anwendung zugänglich“. Der Unterschied ist fein, aber wichtig: Während nur der Entwickler weiß, welcher Speicherbereich in Zukunft zugänglich bleiben soll, können unzugänglich gewordene Speicherbereiche algorithmisch identifiziert und markiert werden, um sie an das Betriebssystem zurückzugeben.
Garbage-free-Sprachen verwenden normalerweise andere Techniken zur Speicherverwaltung: explizite Speicherverwaltung, bei der Entwickler dem Compiler mitteilen, wann ein Speicherbereich nicht mehr benötigt wird, und Referenzzählung, bei der jedem Speicherblock eine Nutzungszählung zugeordnet wird (wann erreicht er Null, wird der Bereich an das OS zurückgegeben). Diese Techniken haben ihre eigenen Gegenstücke (und potenzielle Risiken von Lecks).
Lecks in JavaScript
Der Hauptgrund für Leaks in Garbage-Collection-Sprachen sind unerwünschte Referenzen. Um zu verstehen, was unerwünschte Referenzen sind, müssen wir zunächst verstehen, wie der Garbage Collector bestimmt, ob auf einen Speicherbereich zugegriffen werden kann oder nicht.
„Der Hauptgrund für Leaks in Garbage Collector-basierten Sprachen sind unerwünschte Referenzen“
Markiere und fege
Die meisten Garbage Collectors verwenden einen Algorithmus namens Mark-and-Sweep. Der Algorithmus besteht aus den folgenden Schritten:
- Der Garbage Collector erstellt eine Liste von „Wurzeln“. Roots sind normalerweise globale Variablen, auf die im Code verwiesen wird. In JavaScript ist das "windows"-Objekt ein Beispiel für eine globale Variable, die als Root fungieren kann. Das Windows-Objekt ist immer vorhanden, sodass der Garbage Collector es – und alle seine Kinder – als immer vorhanden (dh nicht als Garbage) betrachten kann.
- Alle Wurzeln werden inspiziert und als aktiv markiert. Alle untergeordneten Elemente werden ebenfalls rekursiv untersucht. Alles, auf das von root aus zugegriffen werden kann, gilt nicht als Müll.
- Alle nicht als aktiv markierten Speicherbereiche können dann als Müll betrachtet werden. Der Collector kann diese Speicherbereiche dann freigeben und an das Betriebssystem zurückgeben.
Die meisten modernen Garbage Collectors bauen mit Variationen auf diesem Algorithmus auf, aber das Prinzip bleibt gleich: Alle zugänglichen Speicherbereiche werden als solche gekennzeichnet und der Rest gilt als Müll.
Unerwünschte Verweise sind Verweise auf Speicherbereiche, von denen der Entwickler weiß, dass sie nicht mehr verwendet werden, die aber – aus einer ganzen Reihe von Gründen – im Baum einer aktiven Wurzel verbleiben. Unerwünschte Referenzen im Kontext von JavaScript sind Variablen, die irgendwo im Code aufbewahrt werden, die nicht mehr verwendet werden und auf einen Speicherbereich verweisen, der hätte freigegeben werden können. Für einige ist es ein Entwicklerfehler.
Um zu verstehen, was die häufigsten Lecks in JavaScript sind, müssen Sie sich ansehen, wie Referenzen oft vergessen werden.

Die vier Arten von JavaScript-Leaks:
1: versehentlich globale Variablen
Eines der Ziele von JavaScript war es, eine Sprache zu entwickeln, die wie Java aussieht, aber offen genug ist, um von Anfängern verwendet zu werden. Dies spiegelt sich in der Art wider, wie JavaScript mit nicht deklarierten Variablen umgeht: Eine Referenz auf eine nicht deklarierte Variable erstellt eine neue Variable im globalen Objekt. Im Falle eines Browsers ist das globale Objekt window. Mit anderen Worten:
function foo(arg) {
bar = "this is a hidden global variable";
}
Is in fact:
function foo(arg) {
window.bar = "this is an explicit global variable";
}
Wenn bar den Verweis auf eine Variable nur innerhalb des Gültigkeitsbereichs der foo-Funktion enthalten sollte und Sie vergessen, sie zu deklarieren, wird versehentlich eine globale Variable erstellt. In diesem Beispiel ist das Lecken eines Strings nicht sehr gefährlich, aber es könnte viel schlimmer sein.
Eine andere Möglichkeit, versehentlich eine globale Variable zu erstellen, ist:
function foo() {
this.variable = "potential accidental global";
}
// Foo called on its own, this points to the global object (window)
// rather than being undefined.
foo();
Um zu verhindern, dass diese Fehler erscheinen, fügen Sie „use strict“ hinzu; am Anfang Ihrer JavaScript-Dateien. Dies löst einen strengeren JavaScript-Parsing-Modus aus, der versehentlich Globals verhindert.
Hinweis zu globalen Variablen
Obwohl wir über unerwartete Globals sprechen, ist die Realität, dass viel Code mit expliziten globalen Variablen übersät ist. Sie sind per Definition nicht einziehbar (es sei denn, sie werden annulliert oder neu zugewiesen). Insbesondere globale Variablen, die zum Speichern und Verarbeiten großer Informationsmengen verwendet werden, sind von Bedeutung. Wenn Sie zum Speichern vieler Daten globale Variablen verwenden müssen, stellen Sie sicher, dass Sie sie auf Null setzen oder neu zuweisen, wenn Sie fertig sind.
Eine häufige Ursache für erhöhten Speicherverbrauch in Bezug auf Globals ist das Caching. Der Cache speichert Daten, die wiederholt verwendet werden. Damit dies wirksam ist, muss der Cache eine größere Größenbeschränkung haben. Unendlich wachsende Caches führen zu einem hohen Speicherverbrauch, da Inhalte nicht gesammelt werden können.
2: Vergessene Timer oder Rückrufe
Die Verwendung von setInterval ist in JavaScript weit verbreitet. Die anderen Bibliotheken bieten Beobachter und andere Systeme, die Rückrufe unterstützen. Die meisten dieser Bibliotheken machen Rückrufe unzugänglich, nachdem ihre Instanzen selbst unzugänglich geworden sind. Im Fall von setInterval ist es üblich, Code wie diesen zu sehen:
var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
// Do stuff with node and someResource.
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);
Dieses Beispiel veranschaulicht, was mit baumelnden Timern passieren kann: Timer, die auf einen Knoten oder Daten verweisen, die nicht mehr benötigt werden. Das durch den Knoten dargestellte Objekt kann in Zukunft gelöscht werden, wodurch der gesamte Block innerhalb des Bereichshandlers unbrauchbar wird.
Allerdings ist der Handler, ebenso wie das Intervall, noch aktiv und kann daher nicht abgeholt werden (dazu müsste das Intervall gestoppt werden). Wenn der Bereich nicht erfasst werden kann, können seine Abhängigkeiten auch nicht erfasst werden. Das bedeutet, dass die Ressourcen, die möglicherweise recht umfangreiche Daten speichern, auch nicht gesammelt werden können.
Im Fall von Beobachtern ist es wichtig, Aufrufe explizit zu machen, um sie zu entfernen, wenn sie nicht mehr benötigt werden (oder die zugehörigen Objekte kurz davor stehen, unerreichbar zu werden). In der Vergangenheit war dies besonders wichtig, da einige Browser (IE6) nicht in der Lage waren, mit zyklischen Referenzen gut umzugehen (weitere Informationen dazu siehe unten).
Heutzutage können und werden die meisten Browser Beobachter-Handler sammeln, sobald das beobachtete Objekt nicht mehr erreichbar ist. Es empfiehlt sich jedoch, Beobachter zu löschen, bevor das Objekt gelöscht wird. Beispielsweise :
var element = document.getElementById('button');
function onClick(event) {
element.innerHtml = 'text';
}
element.addEventListener('click', onClick);
// Do stuff
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);
// Now when element goes out of scope,
// both element and onClick will be collected even in old browsers that don't
// handle cycles well.
Hinweis zu Objektbeobachtern und zyklischen Referenzen
Beobachter und zyklische Verweise sind seit langem der Albtraum eines JavaScript-Entwicklers. Dies war auf einen Fehler (oder eine Entwurfsentscheidung) im Garbage Collector von Internet Explorer zurückzuführen. Ältere Versionen von IE konnten keine zyklischen Verweise zwischen DOM-Knoten und JavaScript-Code erkennen. Dies ist charakteristisch für Beobachter, die in der Regel einen Bezug zum Beobachtbaren halten (wie im obigen Beispiel). Mit anderen Worten, jedes Mal, wenn ein Beobachter zu einem Knoten im Internet Explorer hinzugefügt wird, führt dies zu einem Speicherleck. Aus diesem Grund haben Entwickler damit begonnen, Handler vor Knoten manuell zu entfernen oder Referenzen in Beobachtern auf Null zu setzen. Heutzutage verwenden moderne Browser (einschließlich Internet Explorer und Microsoft Edge) modernere Garbage-Collection-Algorithmen, die diese Zyklen erkennen und korrekt behandeln können. Mit anderen Worten, es ist nicht mehr erforderlich, removeEventListener aufzurufen, bevor ein Knoten unerreichbar gemacht wird.
Frameworks wie jQuery entfernen Listener, bevor sie Knoten freigeben (wenn sie dafür ihre spezifische API verwenden). Es wird intern von der Bibliothek gehandhabt und stellt sicher, dass es auch bei älteren Browsern wie dem alten Internet Explorer keine Lecks gibt.
3: Verweise außerhalb des DOM
Manchmal kann es sinnvoll sein, DOM-Knoten in Datenstrukturen zu speichern. Angenommen, Sie möchten den Inhalt mehrerer Spalten in einer Tabelle schnell aktualisieren. Es kann sinnvoll sein, eine Referenz auf jede DOM-Spalte in einem Wörterbuch oder einem Array zu speichern. In diesem Fall werden zwei Verweise auf dasselbe DOM-Element beibehalten: einer im DOM-Baum und der andere im Wörterbuch. Wenn Sie sich zu einem späteren Zeitpunkt entscheiden, diese Spalten zu löschen, müssen Sie beide Verweise unzugänglich machen.
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image'),
text: document.getElementById('text')
};
function doStuff() {
image.src = 'http://some.url/image';
button.click();
console.log(text.innerHTML);
// Much more logic
}
function removeButton() {
// The button is a direct child of body.
document.body.removeChild(document.getElementById('button'));
// At this point, we still have a reference to #button in the global
// elements dictionary. In other words, the button element is still in
// memory and cannot be collected by the GC.
}
Eine weitere zu berücksichtigende Sache sind Verweise auf interne Knoten oder Blätter des DOM-Baums. Angenommen, Sie behalten beispielsweise einen Verweis auf eine bestimmte Tabellenzelle (a -Tag) in Ihrem JavaScript-Code. Irgendwann entscheiden Sie sich, die bestimmte Tabelle aus dem DOM zu entfernen, aber den Verweis auf diese Zelle beizubehalten. Intuitiv würde man meinen, dass der GC alles außer dieser Zelle korrekt erfassen wird. In der Praxis wird dies nicht passieren: Diese Zelle ist ein untergeordneter Knoten des Arrays und die untergeordneten Elemente behalten Verweise auf ihre übergeordneten Elemente. Das bedeutet, dass die gesamte Tabelle aufgrund der JavaScript-Referenz auf diese Zelle im Speicher bleibt. Berücksichtigen Sie dies, wenn Sie Verweise auf DOM-Elemente beibehalten.
4: Schließen
Ein Schlüsselelement der JavaScript-Entwicklung ist die Schließung: anonyme Funktionen, die Variablen aus dem übergeordneten Gültigkeitsbereich erfassen. Die Meteor-Entwickler stießen aufgrund der Details der Javascript-Laufzeitimplementierung auf einen speziellen, eher subtilen Fall von Speicherlecks:
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing)
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log(someMessage);
}
};
};
setInterval(replaceThing, 1000);
Dieses Snippet macht eines: Wann immer Ding ersetzen wird genannt, die Sache ruft ein neues Objekt ab, das ein großes Array und einen neuen Abschluss (etwasMethode). Gleichzeitig enthält die unbenutzte Variable einen Zaun, auf den verwiesen wird originelles Ding (die Sache vom vorherigen Anruf an Ding ersetzen). Schon etwas kompliziert, oder? Wichtig ist, dass dieser Bereich gemeinsam genutzt wird, sobald der Bereich für die Zäune erstellt wurde, die sich im selben übergeordneten Bereich befinden.
In diesem Fall wird der Bereich für die Schließung erstellt etwasMethode mit geteilt wird ungenutzt. ungebraucht hat einen Bezug zu originelles Ding. Obwohl ungenutzt niemals verwendet werden. etwasMethode durchgenutzt werden kann die Sache. Und wie etwasMethode teilt den Umfang des Zauns mit ungenutztselbst wenn ungenutzt wird nie verwendet, seine Bezugnahme auf originelles Ding zwingt es, aktiv zu bleiben (verhindert seine Erfassung).
Wenn dieses Snipet wiederholt ausgeführt wird, sehen wir einen stetigen Anstieg der Speichernutzung. Und es verringert sich nicht mit der Verabschiedung des GC. Im Wesentlichen wird eine schließende verkettete Liste erstellt (die durch die Variable verwurzelt ist die Sache) und jeder Geltungsbereich dieser Closures enthält einen Verweis auf das große Array, was ein konsequentes Leck erzeugt.
Dies ist ein Implementierungsartefakt. Eine andere Implementierung von Zäunen, die dieses Problem handhaben würde, ist denkbar, wie in erläutert Meteor-Blog.
Originalartikel de Sebastian Peyrott übersetzt von JS Staff
Verpassen Sie nicht die Fortsetzung der nächsten Folge: So beheben Sie Speicherlecks 🙂
[separator type=““ size=““ icon=“star“] [actionbox color=“default“ title=““ description=“JS-REPUBLIC ist ein auf JavaScript-Entwicklung spezialisiertes Dienstleistungsunternehmen. Wir sind ein anerkanntes Ausbildungszentrum. Alle unsere technischen Schulungen finden Sie auf unserer Partnerseite für Schulungen“ btn_label=“Unser Training“ btn_link=“http://training.ux-republic.com“ btn_color=“primary“ btn_size=“big“ btn_icon=“star“ btn_external ="1"]
