In der Spalte „Javascript“ wird ein weiteres wichtiges Thema besprochen: Speicherverwaltung
ÜbersichtProgrammiersprachen wie C verfügen über Speicherverwaltungsprimitive auf niedriger Ebene wie malloc() und free(). Entwickler verwenden diese Grundelemente, um dem Betriebssystem explizit Speicher zuzuweisen und freizugeben.
JavaScript reserviert Speicher für Objekte (Objekte, Zeichenfolgen usw.), wenn diese erstellt werden, und gibt den Speicher „automatisch“ frei, wenn sie nicht mehr verwendet werden. Dieser Vorgang wird als Garbage Collection bezeichnet. Diese scheinbar „automatische“ Freigabe von Ressourcen sorgt für Verwirrung, da sie den Entwicklern von JavaScript (und anderen Hochsprachen) den falschen Eindruck vermittelt, dass ihnen die Speicherverwaltung egal ist. Das ist ein großer Fehler.Hier finden Sie eine kurze Einführung in jede Phase des Speicherlebenszyklus:Auch wenn Entwickler mit Hochsprachen arbeiten, sollten sie sich mit der Speicherverwaltung auskennen (oder zumindest die Grundlagen kennen). Manchmal gibt es Probleme mit der automatischen Speicherverwaltung (z. B. Fehler im Garbage Collector oder Implementierungsbeschränkungen usw.), und Entwickler müssen diese Probleme verstehen, damit sie richtig behandelt werden können (oder eine geeignete Lösung finden, die mit minimalem Wartungsaufwand gewartet werden kann Aufwandscode). Der Lebenszyklus des SpeichersEgal welche Programmiersprache verwendet wird, der Lebenszyklus des Speichers ist derselbe:
Zuordnen Speicher – Speicher wird vom Betriebssystem zugewiesen, sodass Ihr Programm ihn verwenden kann. In einer Low-Level-Sprache (wie C) ist dies eine explizit ausgeführte Operation, die der Entwickler selbst durchführen muss. In Hochsprachen weist das System jedoch automatisch die intrinsischen Sprachen zu.
Speicher freigeben
– Geben Sie den gesamten ungenutzten Speicher frei, sodass er freier Speicher wird und wiederverwendet werden kann. Ebenso wie die Zuweisung von Speicheroperationen muss diese Operation auch in Low-Level-Sprachen explizit ausgeführt werden.Bevor wir Speicher in JavaScript vorstellen, werden wir kurz besprechen, was Speicher ist und wie er funktioniert.
Auf Hardwareebene wird der Computerspeicher durch eine große Anzahl von Triggern zwischengespeichert. Jedes Flip-Flop enthält mehrere Transistoren, die ein Bit speichern können, und einzelne Flip-Flops sind durch eine eindeutige Kennung adressierbar, sodass wir sie lesen und überschreiben können. Daher kann der gesamte Computerspeicher konzeptionell als ein riesiges Array betrachtet werden, das gelesen und beschrieben werden kann.
Als Menschen sind wir nicht sehr gut darin, in Bits zu denken und zu rechnen, deshalb organisieren wir sie in größeren Gruppen, die zusammen zur Darstellung von Zahlen verwendet werden können. 8 Bits werden als 1 Byte bezeichnet. Neben Bytes gibt es auch Wörter (manchmal 16 Bit, manchmal 32 Bit). Vieles ist im Speicher gespeichert:Dieser Code zeigt die Speichergröße an, die von Ganzzahlvariablen und Gleitkommavariablen mit doppelter Genauigkeit belegt wird. Aber vor etwa 20 Jahren belegten Integer-Variablen normalerweise 2 Bytes, während Gleitkommavariablen mit doppelter Genauigkeit 4 Bytes belegten. Ihr Code sollte nicht von der Größe des aktuellen primitiven Datentyps abhängen.
Der Compiler fügt Code ein, der mit dem Betriebssystem interagiert, und weist die Anzahl der Stapelbytes zu, die zum Speichern von Variablen erforderlich sind.
Im obigen Beispiel kennt der Compiler die genaue Speicheradresse jeder Variablen. Tatsächlich wird jedes Mal, wenn wir in die Variable n
schreiben, diese intern in Informationen wie "Speicheradresse 4127963"
umgewandelt. n
时,它就会在内部被转换成类似“内存地址4127963”
这样的信息。
注意,如果我们尝试访问 x[4]
x[4]
zuzugreifen, auf die mit m verknüpften Daten zugegriffen wird. Dies liegt daran, dass beim Zugriff auf ein nicht vorhandenes Element im Array (das 4 Byte größer ist als das letzte tatsächlich zugewiesene Element im Array, x[3]) möglicherweise einige m Bits gelesen (oder überschrieben) werden. Dies wird sicherlich unvorhersehbare Auswirkungen auf den Rest des Programms haben.
Wenn Funktionen andere Funktionen aufrufen, erhält jede Funktion ihren eigenen Block auf dem Aufrufstapel. Es enthält alle lokalen Variablen, verfügt aber auch über einen Programmzähler, der sich während der Ausführung daran erinnert, wo es sich befindet. Wenn die Funktion abgeschlossen ist, wird ihr Speicherblock an anderer Stelle erneut verwendet. Dynamische ZuweisungLeider wird es etwas kompliziert, wenn man zum Zeitpunkt der Kompilierung nicht weiß, wie viel Speicher eine Variable benötigt. Angenommen, wir möchten Folgendes tun:
Zur Kompilierungszeit weiß der Compiler nicht, wie viel Speicher das Array verwenden muss, da dies durch den vom Benutzer bereitgestellten Wert bestimmt wird. Daher kann kein Platz für Variablen auf dem Stapel reserviert werden. Stattdessen muss unser Programm zur Laufzeit explizit den entsprechenden Speicherplatz vom Betriebssystem anfordern. Dieser Speicher wird aus dem Heap-Speicherplatz
zugewiesen. Der Unterschied zwischen statischer Speicherzuweisung und dynamischer Speicherzuweisung ist in der folgenden Tabelle zusammengefasst:Statische Speicherzuweisung | Dynamische Speicherzuweisung |
---|---|
Die Größe muss zur Kompilierzeit bekannt sein | Die Größe muss nicht bekannt sein zur Kompilierzeit bekannt sein |
Zur Kompilierungszeit ausgeführt | Zur Laufzeit ausgeführt |
Auf dem Stapel zugewiesen | Auf dem Heap zugewiesen |
FILO (zuerst rein, zuletzt raus) | Keine spezifische Reihenfolge von Zuteilung |
Um die Funktionsweise der dynamischen Speicherzuweisung vollständig zu verstehen, müssen Sie sich mehr mit Zeigern befassen, was möglicherweise zu stark vom Thema dieses Artikels abweicht. Ich werde das entsprechende Wissen über Zeiger hier nicht im Detail vorstellen.
Jetzt wird der erste Schritt erklärt: Wie man Speicher in JavaScript zuweist.
JavaScript entlastet Entwickler von der Verantwortung für die manuelle Speicherzuweisung – JavaScript weist Speicher zu und deklariert Werte selbst.
Bestimmte Funktionsaufrufe führen auch zur Speicherzuweisung von Objekten:
Methoden können neue Werte oder Objekte zuordnen:
Zugeordneten Speicher in JavaScript verwenden Bedeutet Lesen und Schreiben darin, was durch Lesen oder Schreiben des Werts einer Variablen oder Objekteigenschaft oder durch Übergeben von Parametern an eine Funktion erreicht werden kann.
Die meisten Speicherverwaltungsprobleme treten in dieser Phase auf.
Der schwierigste Teil hierbei ist die Bestimmung, wann der zugewiesene Speicher nicht mehr benötigt wird. Normalerweise muss der Entwickler bestimmen, wo sich der Speicher befindet nicht mehr benötigt und geben Sie es frei.
Hochsprachen integrieren einen Mechanismus namens Garbage Collector, dessen Aufgabe darin besteht, die Speicherzuweisung und -nutzung zu verfolgen, um jederzeit zu erkennen, wann ein Teil des zugewiesenen Inneren nicht mehr benötigt wird. In diesem Fall wird dieser Speicher automatisch freigegeben.
Leider handelt es sich bei diesem Vorgang nur um eine grobe Schätzung, da es schwierig ist zu wissen, ob ein bestimmtes Stück Speicher wirklich benötigt wird (kann nicht algorithmisch gelöst werden).
Die meisten Garbage Collectors arbeiten, indem sie Speicher sammeln, auf den nicht mehr zugegriffen wird, d. h. alle Variablen, die darauf verweisen, sind außerhalb des Gültigkeitsbereichs. Allerdings ist dies eine Unterschätzung des Speicherplatzes, der gesammelt werden kann, da es an jedem Punkt eines Speicherorts immer noch eine Variable im Gültigkeitsbereich geben kann, die darauf verweist, auf die jedoch nie wieder zugegriffen wird.
Da es unmöglich ist, festzustellen, ob ein Teil des Speichers wirklich nützlich ist, hat sich der Garbage Collector eine Möglichkeit überlegt, dieses Problem zu lösen. In diesem Abschnitt werden die wichtigsten Garbage-Collection-Algorithmen und ihre Einschränkungen erläutert und erläutert.
Garbage-Collection-Algorithmen basieren hauptsächlich auf Referenzen.
Im Zusammenhang mit der Speicherverwaltung spricht man von einem Objekt, das auf ein anderes Objekt verweist, wenn es Zugriff auf ein anderes Objekt hat (entweder implizit oder explizit). Beispielsweise verfügt ein JavaScript-Objekt über Verweise auf seinen Prototyp (implizite Referenz) und Eigenschaftswerte (explizite Referenz).
In diesem Zusammenhang wird das Konzept des „Objekts“ auf einen größeren Bereich als normale JavaScript-Objekte erweitert und umfasst auch den Funktionsbereich (oder den globalen lexikalischen Bereich).
Der lexikalische Gültigkeitsbereich definiert, wie Variablennamen innerhalb verschachtelter Funktionen aufgelöst werden: Die innere Funktion enthält die Auswirkung der übergeordneten Funktion, auch wenn die übergeordnete Funktion zurückgegeben wurde.
Dies ist der einfachste Garbage-Collection-Algorithmus. Wenn kein Verweis auf ein Objekt vorhanden ist, gilt das Objekt als „Garbage Collector“, wie im folgenden Code:
Bei Schleifen gibt es eine Grenze. Im folgenden Beispiel werden zwei Objekte erstellt, die aufeinander verweisen und so eine Schleife erzeugen. Nachdem der Funktionsaufruf den Gültigkeitsbereich verlassen hat, sind sie praktisch nutzlos und können freigegeben werden. Der Referenzzählalgorithmus geht jedoch davon aus, dass keines von ihnen durch Garbage Collection erfasst werden kann, da jedes Objekt mindestens einmal referenziert wird.
Dieser Algorithmus kann bestimmen, ob auf ein Objekt zugegriffen werden kann, um festzustellen, ob das Objekt nützlich ist:
Dieser Algorithmus ist besser als der vorherige Algorithmus, da „auf ein Objekt nicht verwiesen wird“ bedeutet, dass auf das Objekt nicht zugegriffen werden kann.
Seit 2012 verfügen alle modernen Browser über Garbage Collectors, die Markierungen löschen. Alle in den letzten Jahren im Bereich der JavaScript-Garbage Collection (generationelle/inkrementelle/gleichzeitige/parallele Garbage Collection) vorgenommenen Verbesserungen waren Implementierungsverbesserungen des Algorithmus (Mark-Sweep) und keine Verbesserungen des Garbage Collection-Algorithmus selbst Ist es das Ziel, festzustellen, ob ein Objekt zugänglich ist?
In diesem Artikel erfahren Sie mehr über die Verfolgung der Garbage Collection, einschließlich des Mark-Sweep-Algorithmus und seiner Optimierungen.
Im ersten Beispiel oben werden die beiden Objekte nach der Rückkehr des Funktionsaufrufs nicht mehr von Objekten referenziert, auf die vom globalen Objekt aus zugegriffen werden kann. Daher sind sie für den Garbage Collector nicht zugänglich.
Obwohl es Referenzen zwischen Objekten gibt, sind sie vom Wurzelknoten aus nicht erreichbar.
Obwohl Garbage Collectors praktisch sind, haben sie ihre eigenen Kompromisse, von denen einer der Nichtdeterminismus ist. Mit anderen Worten: GC ist unvorhersehbar und man kann nicht wirklich sagen, was wird passieren. Das bedeutet, dass das Programm in manchen Fällen mehr Speicher belegt, als eigentlich nötig ist. Bei besonders geschwindigkeitsempfindlichen Anwendungen kann es zu kurzen Pausen kommen. Wenn kein Speicher zugewiesen ist, ist der größte Teil des GC im Leerlauf. Schauen Sie sich das folgende Szenario an:
In diesen Szenarien werden die meisten GCs nicht weiter sammeln. Mit anderen Worten: Selbst wenn unzugängliche Referenzen zur Sammlung verfügbar sind, wird der Sammler sie nicht deklarieren. Hierbei handelt es sich nicht unbedingt um Lecks, sie können jedoch dennoch zu einer überdurchschnittlichen Speichernutzung führen.
Im Wesentlichen kann ein Speicherverlust definiert werden als: Speicher, der von einer Anwendung nicht mehr benötigt wird und aus irgendeinem Grund nicht an das Betriebssystem oder den freien Speicherpool zurückgegeben wird.
Programmiersprachen unterstützen verschiedene Speicherverwaltungsmethoden. Ob ein bestimmter Teil des Speichers verwendet werden soll, ist jedoch tatsächlich eine unbestimmte Frage. Mit anderen Worten: Nur der Entwickler kann feststellen, ob ein Stück Speicher an das Betriebssystem zurückgegeben werden kann.
Einige Programmiersprachen unterstützen Entwickler, andere erwarten von Entwicklern ein klares Verständnis dafür, wann Speicher nicht mehr verwendet wird. Wikipedia bietet einige großartige Artikel zur manuellen und automatischen Speicherverwaltung.
JavaScript behandelt nicht deklarierte Variablen auf interessante Weise: Für nicht deklarierte Variablen wird im globalen Bereich eine neue Variable erstellt, um eine Referenz zu erstellen. In einem Browser ist das globale Objekt window. Zum Beispiel:
function foo(arg) { bar = "some text"; }
ist äquivalent zu:
function foo(arg) { window.bar = "some text"; }
Wenn bar auf eine Variable im Bereich der foo-Funktion verweist, aber vergisst, sie mit var zu deklarieren, wird eine unerwartete globale Variable erstellt. In diesem Beispiel würde das Fehlen einer einfachen Zeichenfolge nicht viel Schaden anrichten, wäre aber auf jeden Fall schlecht.
Eine andere Möglichkeit, eine unerwartete globale Variable zu erstellen, besteht darin, diese zu verwenden:
function foo() { this.var1 = "potential accidental global"; } // Foo自己调用,它指向全局对象(window),而不是未定义。 foo();
Sie können dies vermeiden, indem Sie am Anfang der JavaScript-Datei „use strict“ hinzufügen, wodurch ein strengerer JavaScript-Parsing-Modus aktiviert wird, um eine versehentliche Erstellung zu verhindern globale Variablen.
Obwohl es sich um unbekannte globale Variablen handelt, gibt es immer noch viele Codes, die mit expliziten globalen Variablen gefüllt sind. Per Definition sind diese nicht einsammelbar (es sei denn, sie sind als leer oder neu zugewiesen angegeben). Von besonderer Bedeutung sind globale Variablen, die zur vorübergehenden Speicherung und Verarbeitung großer Informationsmengen verwendet werden. Wenn Sie eine globale Variable zum Speichern einer großen Datenmenge verwenden müssen, stellen Sie sicher, dass Sie null angeben oder sie neu zuweisen, wenn Sie fertig sind.
Nehmen Sie setInterval
als Beispiel, da es häufig in JavaScript verwendet wird.
var serverData = loadData(); setInterval(function() { var renderer = document.getElementById('renderer'); if(renderer) { renderer.innerHTML = JSON.stringify(serverData); } }, 5000); //每五秒会执行一次
Der obige Codeausschnitt zeigt die Verwendung eines Timers, um auf Knoten oder Daten zu verweisen, die nicht mehr benötigt werden.
Das vom Renderer dargestellte Objekt wird möglicherweise irgendwann in der Zukunft gelöscht, wodurch ein ganzer Codeblock im internen Handler nicht mehr benötigt wird. Da der Timer jedoch noch aktiv ist, können der Handler und seine Abhängigkeiten nicht erfasst werden. Dies bedeutet, dass serverData, das große Datenmengen speichert, nicht erfasst werden kann.
Wenn Sie Beobachter verwenden, müssen Sie sicherstellen, dass Sie einen expliziten Aufruf zum Löschen dieser Beobachter tätigen, nachdem Sie sie nicht mehr verwendet haben (entweder wird der Beobachter nicht mehr benötigt oder das Objekt wird unzugänglich).
Als Entwickler müssen Sie sicherstellen, dass Sie sie explizit löschen, wenn Sie mit ihnen fertig sind (sonst ist der Zugriff auf das Objekt nicht mehr möglich).
In der Vergangenheit konnten einige Browser mit diesen Situationen nicht umgehen (der gute alte IE6). Glücklicherweise erledigen dies die meisten modernen Browser mittlerweile für Sie: Sie sammeln automatisch Beobachter-Handler, sobald auf das beobachtete Objekt nicht mehr zugegriffen werden kann, selbst wenn Sie vergessen, den Listener zu entfernen. Wir sollten diese Beobachter jedoch dennoch explizit entfernen, bevor das Objekt entsorgt wird. Zum Beispiel:
Heutige Browser (einschließlich IE und Edge) verwenden moderne Garbage-Collection-Algorithmen, die diese Zirkelverweise sofort erkennen und verarbeiten können. Mit anderen Worten: Es ist nicht erforderlich, „removeEventListener“ aufzurufen, bevor ein Knoten gelöscht wird.
Einige Frameworks oder Bibliotheken, wie z. B. JQuery, entfernen Listener automatisch, bevor Knoten entsorgt werden (bei Verwendung ihrer spezifischen API). Dies wird durch einen Mechanismus innerhalb der Bibliothek implementiert, der sicherstellt, dass keine Speicherlecks auftreten, selbst wenn es in problematischen Browsern wie IE 6 ausgeführt wird.
Abschlüsse sind ein Schlüsselaspekt der JavaScript-Entwicklung, bei der eine innere Funktion Variablen aus einer äußeren (eingeschlossenen) Funktion verwendet. Aufgrund der Details der Ausführung von JavaScript kann es auf folgende Weise zu Speicherverlusten kommen:
Dieser Code bewirkt eines: Jedes Mal, wenn replaceThing
aufgerufen wird, theThing code > erhält ein neues Objekt, das ein großes Array und einen neuen Abschluss (someMethod) enthält. Gleichzeitig zeigt die Variable <code>unuse
d auf einen Abschluss, der auf `originalThing
verweist. replaceThing
的时候,theThing
都会得到一个包含一个大数组和一个新闭包(someMethod)的新对象。同时,变量unuse
d指向一个引用了`originalThing
的闭包。
是不是有点困惑了? 重要的是,一旦具有相同父作用域的多个闭包的作用域被创建,则这个作用域就可以被共享。
在这种情况下,为闭包someMethod
而创建的作用域可以被unused
共享的。unused
内部存在一个对originalThing
的引用。即使unused
从未使用过,someMethod
也可以在replaceThing
的作用域之外(例如在全局范围内)通过theThing
来被调用。
由于someMethod
共享了unused
闭包的作用域,那么unused
引用包含的originalThing
会迫使它保持活动状态(两个闭包之间的整个共享作用域)。这阻止了它被收集。
当这段代码重复运行时,可以观察到内存使用在稳定增长,当GC
运行后,内存使用也不会变小。从本质上说,在运行过程中创建了一个闭包链表(它的根是以变量theThing
someMethod
erstellte Bereich von unused
gemeinsam genutzt werden. Es gibt einen internen Verweis auf originalThing
innerhalb von unused
. Auch wenn unused
nie verwendet wird, kann someMethod
außerhalb des Geltungsbereichs von replaceThing
an theThing übergeben werden (z. B. im globalen Geltungsbereich). )
aufgerufen werden. someMethod
den Umfang des unused
-Abschlusses teilt, gilt auch der Verweis von unused
auf das enthaltene originalThing
erzwinge, dass es am Leben bleibt (der gesamte gemeinsame Bereich zwischen den beiden Schließungen). Dies verhindert, dass es gesammelt wird. Wenn dieser Code wiederholt ausgeführt wird, können Sie beobachten, dass die Speichernutzung stetig zunimmt. Wenn GC
ausgeführt wird, wird die Speichernutzung nicht kleiner. Im Wesentlichen wird zur Laufzeit eine verknüpfte Liste von Abschlüssen erstellt (ihre Wurzel besteht aus der Variablen theThing
), und der Bereich jedes Abschlusses verweist indirekt auf ein großes Array, was zu einem erheblichen Speicherverlust führte.
4. Referenzen vom DOM lösen
Manchmal kann es nützlich sein, DOM-Knoten in einer Datenstruktur zu speichern. Angenommen, Sie möchten schnell mehrere Zeilen in einer Tabelle aktualisieren, dann können Sie einen Verweis auf jede DOM-Zeile in einem Wörterbuch oder Array speichern. Auf diese Weise gibt es zwei Verweise auf dasselbe DOM-Element: einen im DOM-Baum und einen im Wörterbuch. Wenn Sie sich zu einem späteren Zeitpunkt dazu entschließen, diese Zeilen zu löschen, müssen Sie beide Referenzen unzugänglich machen. 🎜🎜Bei der Referenzierung interner Knoten oder Blattknoten im DOM-Baum ist noch ein weiteres Problem zu berücksichtigen. Wenn Sie in Ihrem Code einen Verweis auf eine Tabellenzelle (Das obige ist der detaillierte Inhalt vonEinführung in die JavaScript-Speicherverwaltung + Umgang mit vier häufigen Speicherlecks. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!