In diesem Abschnitt werden allgemeine Begriffe beschrieben, die in der Arbeitsspeicheranalyse verwendet werden. Er gilt für eine Vielzahl von Tools zur Arbeitsspeicherprofilerstellung für verschiedene Sprachen.
Die hier beschriebenen Begriffe und Informationen beziehen sich auf den Heap-Profiler in den Chrome-Entwicklertools. Wenn Sie schon einmal mit Java, .NET oder einem anderen Memory Profiler gearbeitet haben, ist dies möglicherweise zur Auffrischung.
Objektgrößen
Stellen Sie sich den Speicher als eine Grafik mit primitiven Typen (wie Zahlen und Strings) und Objekten (assoziativen Arrays) vor. Sie kann wie folgt als Diagramm mit einer Reihe miteinander verbundener Punkte visuell dargestellt werden:
Ein Objekt kann sich auf zwei Arten im Gedächtnis speichern:
- direkt am Objekt selbst.
- Implizit durch das Halten von Verweisen auf andere Objekte, wodurch verhindert wird, dass diese Objekte automatisch von einer automatischen Speicherbereinigung (kurz GC) entsorgt werden.
Wenn Sie den Heap Profiler in den Entwicklertools verwenden, ein Tool zur Untersuchung von Speicherproblemen unter „Profile“, werden Sie wahrscheinlich mehrere verschiedene Informationsspalten betrachten. Zwei der wichtigsten sind Shallow Size und Retained Size. Aber was stellen sie dar?
Flache Größe
Dies ist die Größe des Arbeitsspeichers, der vom Objekt selbst gehalten wird.
Bei typischen JavaScript-Objekten ist ein gewisser Arbeitsspeicher für ihre Beschreibung und zum Speichern direkter Werte reserviert. Normalerweise können nur Arrays und Strings eine signifikante flache Größe haben. Der Hauptspeicher von Strings und externen Arrays liegt jedoch oft im Arbeitsspeicher des Renderers, sodass nur ein kleines Wrapper-Objekt auf dem JavaScript-Heap zur Verfügung steht.
Der Renderer-Arbeitsspeicher ist der gesamte Arbeitsspeicher für den Prozess, in dem eine geprüfte Seite gerendert wird: nativer Arbeitsspeicher + JS-Heap-Speicher der Seite + JS-Heap-Speicher aller dedizierten Worker, die von der Seite gestartet werden. Dennoch kann selbst ein kleines Objekt indirekt eine große Menge an Arbeitsspeicher enthalten, da so verhindert wird, dass andere Objekte bei der automatischen Speicherbereinigung entsorgt werden.
Beibehaltene Größe
Dies ist die Größe des Arbeitsspeichers, der freigegeben wird, nachdem das Objekt selbst und seine abhängigen Objekte, die über die GC-Roots nicht erreichbar waren, gelöscht wurden.
GC-Roots bestehen aus Handles, die (entweder lokal oder global) erstellt werden, wenn ein Verweis von nativem Code auf ein JavaScript-Objekt außerhalb von V8 erstellt wird. Alle diese Handles finden Sie in einem Heap-Snapshot unter GC-Roots > Handle range (Umfang des Handles) und GC-Roots > Global Handles. Es kann verwirrend sein, die Aliasse in dieser Dokumentation zu beschreiben, ohne die Browserimplementierung detailliert zu beschreiben. Sie müssen sich weder über die GC-Roots noch über die Handles Gedanken machen.
Es gibt viele interne GC-Root-Zertifikate, von denen die meisten für die Nutzer nicht interessant sind. Aus Sicht der Anwendung gibt es folgende Arten von Wurzeln:
- Globales Window-Objekt (in jedem iFrame). In den Heap-Snapshots befindet sich ein Entfernungsfeld, das die Anzahl der Attributverweise auf dem kürzesten Beibehaltungspfad aus dem Fenster angibt.
- Dokument-DOM-Baum bestehend aus allen nativen DOM-Knoten, die über das Dokument erreichbar sind. Möglicherweise haben nicht alle JS-Wrapper, aber wenn sie vorhanden sind, bleiben die Wrapper aktiv, während das Dokument aktiv ist.
- Manchmal werden Objekte durch den Debugger-Kontext und die Entwicklertools-Konsole beibehalten (z.B. nach der Konsolenauswertung). Erstellen Sie Heap-Snapshots mit klarer Konsole und ohne aktive Haltepunkte im Debugger.
Die Speichergrafik beginnt mit einem Stamm. Das kann das window
-Objekt des Browsers oder das Global
-Objekt eines Node.js-Moduls sein. Sie steuern nicht, wie dieses Stammobjekt durch die automatische Speicherbereinigung erstellt wird.
Alles, was nicht vom Root aus erreichbar ist, erhält automatische Speicherbereinigung.
Objekte, die Baum beibehalten
Der Heap ist ein Netzwerk miteinander verbundener Objekte. In der mathematischen Welt wird diese Struktur als Graph oder Speicherdiagramm bezeichnet. Ein Diagramm wird aus Knoten erstellt, die über Kanten verbunden sind und die beide mit Labels versehen sind.
- Knoten (oder Objekte) werden mit dem Namen der Konstruktorfunktion gekennzeichnet, mit der sie erstellt wurden.
- Kanten werden mit den Namen der Eigenschaften beschriftet.
Profil mit dem Heap Profiler aufzeichnen Zu den auffälligen Dingen, die wir in der Heap Profiler-Aufzeichnung unten sehen können, gehört die Entfernung, also die Entfernung vom GC-Stamm. Wenn fast alle Objekte desselben Typs die gleiche Entfernung und einige Objekte in größerer Entfernung haben, lohnt es sich, dies genauer zu untersuchen.
Dominatoren
Dominator-Objekte bestehen aus einer Baumstruktur, da jedes Objekt genau einen Dominator hat. Einem Dominator eines Objekts fehlen unter Umständen direkte Verweise auf ein von ihm dominiertes Objekt. Das bedeutet, dass der Baum des Dominators keine Spannweite des Graphen ist.
Im folgenden Diagramm gilt Folgendes:
- Knoten 1 dominiert Knoten 2
- Node 2 dominiert die Knoten 3, 4 und 6
- Node 3 dominiert Knoten 5
- Node 5 dominiert Knoten 8
- Node 6 dominiert Knoten 7
Im folgenden Beispiel ist der Knoten #3
der Dominator von #10
, aber #7
existiert auch in jedem einfachen Pfad von GC zu #10
. Daher ist Objekt B Dominator von Objekt A, wenn B in jedem einfachen Pfad vom Stamm bis zum Objekt A vorhanden ist.
V8-Besonderheiten
Bei der Erstellung von Arbeitsspeicherprofilen ist es hilfreich zu verstehen, warum Heap-Snapshots ein bestimmtes Aussehen haben. In diesem Abschnitt werden einige speicherbezogene Themen beschrieben, die sich speziell auf die V8-JavaScript-VM (V8-VM oder -VM) beziehen.
JavaScript-Objektdarstellung
Es gibt drei primitive Typen:
- Zahlen (z.B. 3.14159..)
- Boolesche Werte (wahr oder falsch)
- Strings (z.B. „Werner Heisenberg“
Sie können nicht auf andere Werte verweisen und sind immer Blätter oder Endknoten.
Zahlen können so gespeichert werden:
- unmittelbare 31-Bit-Ganzzahlwerte, die als kleine Ganzzahlen (SMIs) bezeichnet werden, oder
- Heap-Objekte, die als Heap-Nummern bezeichnet werden. Heap-Nummern werden zum Speichern von Werten verwendet, die nicht in die SMI-Form passen, z. B. Doubles, oder wenn ein Wert mit Boxen versehen werden muss, z. B. wenn Eigenschaften dafür festgelegt werden sollen.
Strings können an folgenden Stellen gespeichert werden:
- den VM-Heap oder
- extern im Speicher des Renderers gespeichert. Ein Wrapper-Objekt wird erstellt und für den Zugriff auf den externen Speicher verwendet. Dort werden beispielsweise Skriptquellen und andere Inhalte aus dem Web gespeichert, anstatt auf den VM-Heap zu kopieren.
Arbeitsspeicher für neue JavaScript-Objekte wird von einem dedizierten JavaScript-Heap (oder VM-Heap) zugewiesen. Diese Objekte werden von der automatischen Speicherbereinigung von V8 verwaltet und bleiben so lange aktiv, wie es mindestens einen starken Verweis auf sie gibt.
Native Objekte sind alle anderen Objekte, die sich nicht im JavaScript-Heap befinden. Ein natives Objekt wird im Gegensatz zum Heap-Objekt nicht während seiner Lebensdauer vom automatischen Speicherbereinigungsdienst von V8 verwaltet und kann nur über JavaScript mit seinem JavaScript-Wrapper-Objekt aufgerufen werden.
Der Cons-String ist ein Objekt, das aus Paaren von Strings besteht, die gespeichert und dann verknüpft werden, und das Ergebnis der Verkettung. Die Inhalte des cons-Strings werden nur bei Bedarf zusammengeführt. Ein Beispiel hierfür wäre, wenn ein Teilstring eines verknüpften Strings erstellt werden muss.
Wenn Sie beispielsweise a und b verketten, erhalten Sie einen String (a, b), der das Ergebnis der Verkettung darstellt. Wenn Sie d später mit diesem Ergebnis verkettet haben, erhalten Sie einen weiteren Kons-String ((a, b), d).
Arrays: Ein Array ist ein Objekt mit numerischen Schlüsseln. Sie werden in der V8-VM intensiv zum Speichern großer Datenmengen verwendet. Gruppen von Schlüssel/Wert-Paaren, die wie Wörterbücher verwendet werden, werden durch Arrays gesichert.
Ein typisches JavaScript-Objekt kann einer von zwei Arraytypen sein, die zum Speichern verwendet werden:
- benannten Properties und
- numerische Elemente
Falls nur sehr wenige Attribute vorhanden sind, können diese intern im JavaScript-Objekt selbst gespeichert werden.
Map: ein Objekt, das die Art des Objekts und sein Layout beschreibt. Zum Beispiel werden Zuordnungen verwendet, um implizite Objekthierarchien für schnellen Property-Zugriff zu beschreiben.
Objektgruppen
Jede Gruppe nativer Objekte besteht aus Objekten, die gegenseitige Verweise aufeinander enthalten. Nehmen wir zum Beispiel eine DOM-Unterstruktur, bei der jeder Knoten eine Verknüpfung zum übergeordneten Element und zum nächsten untergeordneten und nächsten gleichgeordneten Knoten hat, wodurch ein verbundener Graph entsteht. Beachten Sie, dass native Objekte im JavaScript-Heap nicht dargestellt werden. Deshalb haben sie die Größe null. Stattdessen werden Wrapper-Objekte erstellt.
Jedes Wrapper-Objekt enthält einen Verweis auf das entsprechende native Objekt, um Befehle an dieses weiterzuleiten. Eine Objektgruppe wiederum enthält Wrapper-Objekte. Dadurch entsteht jedoch kein Zyklus, der nicht erfasst werden kann, da GC so intelligent ist, dass Objektgruppen freigegeben werden, deren Wrapper nicht mehr referenziert werden. Wenn Sie jedoch vergessen, einen einzelnen Wrapper freizugeben, werden die gesamte Gruppe und die zugehörigen Wrapper enthalten.