Speicherterminologie

Meggin Kearney
Meggin Kearney

In diesem Abschnitt werden gängige Begriffe beschrieben, die bei der Speicheranalyse verwendet werden. Er ist auf eine Vielzahl von Speicherprofilierungstools für verschiedene Sprachen anwendbar.

Die hier beschriebenen Begriffe beziehen sich auf den Heap-Profiler in den Chrome-Entwicklertools. Wenn Sie schon einmal mit dem Java-, .NET- oder einem anderen Speicher-Profiler gearbeitet haben, ist dies möglicherweise eine Auffrischung.

Objektgrößen

Stellen Sie sich den Arbeitsspeicher als Graph mit primitiven Typen (z. B. Zahlen und Strings) und Objekten (assoziative Arrays) vor. Sie kann visuell als Diagramm mit einer Reihe von miteinander verbundenen Punkten dargestellt werden:

Visuelle Darstellung des Arbeitsspeichers.

Ein Objekt kann auf zwei Arten Speicher belegen:

  • Direkt vom Objekt selbst.
  • Implizit, indem sie Verweise auf andere Objekte halten und so verhindern, dass diese Objekte automatisch von einem Garbage Collector (kurz GC) gelöscht werden.

Wenn Sie mit dem Heap-Profiler in DevTools arbeiten (ein Tool zum Untersuchen von Speicherproblemen, das sich im Bereich Speicher befindet), sehen Sie sich wahrscheinlich einige verschiedene Informationsspalten an. Zwei davon sind Schmale Größe und Beibehaltene Größe. Was bedeuten diese?

Die Spalten „Shallow“ und „Retained Size“ im Bereich „Memory“

Flache Größe

Das ist die Größe des Arbeitsspeichers, die vom Objekt selbst belegt wird.

Für typische JavaScript-Objekte wird ein Teil des Arbeitsspeichers für ihre Beschreibung und zum Speichern von unmittelbaren Werten reserviert. Normalerweise können nur Arrays und Strings eine deutlich geringere Größe haben. Strings und externe Arrays werden jedoch häufig hauptsächlich im Renderer-Speicher gespeichert und nur ein kleines Wrapper-Objekt wird im JavaScript-Heap freigegeben.

Der Renderer-Speicher umfasst den gesamten Arbeitsspeicher des Prozesses, 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 wurden. Dennoch kann auch ein kleines Objekt indirekt eine große Menge an Speicher belegen, indem es verhindert, dass andere Objekte durch die automatische Garbage Collection gelöscht werden.

Beibehaltene Größe

Das ist die Größe des Arbeitsspeichers, der freigegeben wird, sobald das Objekt selbst zusammen mit seinen abhängigen Objekten gelöscht wurde, die von den GC-Stammobjekten nicht mehr erreichbar sind.

GC-Wurzeln bestehen aus Handles, die entweder lokal oder global erstellt werden, wenn eine Referenz von nativem Code auf ein JavaScript-Objekt außerhalb von V8 erstellt wird. Alle diese Handles finden Sie in einem Heap-Snapshot unter GC-Wurzeln > Handle-Umfang und GC-Wurzeln > Globale Handles. Die Handles in dieser Dokumentation zu beschreiben, ohne auf Details der Browserimplementierung einzugehen, kann verwirrend sein. Weder GC-Wurzeln noch Handles sind ein Problem.

Es gibt viele interne GC-Roots, von denen die meisten für die Nutzer nicht interessant sind. Aus Sicht der Anwendungen gibt es die folgenden Arten von Wurzeln:

  • Globales Fensterobjekt (in jedem iFrame). In den Heap-Snapshots gibt es ein Feld für die Entfernung, das die Anzahl der Property-Referenzen auf dem kürzesten Weg vom Fenster aus angibt.
  • Der DOM-Baum des Dokuments, der alle nativen DOM-Knoten enthält, die durch Durchlaufen des Dokuments erreichbar sind. Nicht alle haben möglicherweise JS-Wrapper, aber wenn ja, sind die Wrapper aktiv, solange das Dokument aktiv ist.
  • Manchmal werden Objekte vom Debugger-Kontext und der Entwicklertools-Konsole beibehalten (z.B. nach der Konsolenerstellung). Erstellen Sie Heap-Snapshots mit einer leeren Konsole und ohne aktive Haltepunkte im Debugger.

Der Speichergraph beginnt mit einem Stamm, der das window-Objekt des Browsers oder das Global-Objekt eines Node.js-Moduls sein kann. Sie haben keine Kontrolle darüber, wie dieses Stammobjekt vom GC verarbeitet wird.

Das Stammobjekt kann nicht gesteuert werden.

Alles, was nicht vom Stamm aus erreichbar ist, wird vom Garbage Collector gelöscht.

Objektstruktur beibehalten

Der Heap ist ein Netzwerk miteinander verbundener Objekte. In der Mathematik wird diese Struktur als Graph oder Speichergraph bezeichnet. Ein Graph besteht aus Knoten, die durch Kanten verbunden sind. Beide werden mit Labels versehen.

  • Knoten (oder Objekte) werden mit dem Namen der Konstruktorfunktion gekennzeichnet, mit der sie erstellt wurden.
  • Kanten werden mit den Namen von Properties gekennzeichnet.

Weitere Informationen zum Erstellen eines Heap-Profils In der folgenden Heap-Profiler-Aufzeichnung fällt unter anderem die Entfernung auf: die Entfernung vom GC-Stamm. Wenn sich fast alle Objekte desselben Typs in derselben Entfernung befinden und einige in größerer Entfernung, sollten Sie das genauer untersuchen.

Beispiel für den Abstand vom Stamm

Dominator

Dominatorobjekte bestehen aus einer Baumstruktur, da jedes Objekt genau einen Dominator hat. Ein Dominator eines Objekts kann keine direkten Verweise auf ein Objekt enthalten, das er dominiert. Das heißt, der Baum des Dominators ist kein Spannbaum des Graphen.

Im folgenden Diagramm geschieht Folgendes:

  • Knoten 1 dominiert Knoten 2
  • Knoten 2 dominiert die Knoten 3, 4 und 6
  • Knoten 3 dominiert Knoten 5
  • Knoten 5 dominiert Knoten 8
  • Knoten 6 dominiert Knoten 7

Dominator-Baumstruktur

Im folgenden Beispiel ist Knoten #3 der Dominator von #10, aber #7 ist auch in jedem einfachen Pfad von GC zu #10 vorhanden. Daher ist ein Objekt B ein Dominator eines Objekts A, wenn B in jedem einfachen Pfad von der Wurzel zum Objekt A vorhanden ist.

Animierte Abbildung eines Dominators.

V8-spezifische Informationen

Beim Speicher-Profiling ist es hilfreich zu verstehen, warum Heap-Snapshots so aussehen, wie sie aussehen. In diesem Abschnitt werden einige speicherbezogene Themen beschrieben, die sich speziell auf die V8-JavaScript-virtuelle Maschine (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 Endknoten oder Blätter.

Zahlen können so gespeichert werden:

  • eine unmittelbare 31-Bit-Ganzzahl, die als Small Integer (SMIs) bezeichnet wird, oder
  • Heap-Objekte, die als Heap-Nummern bezeichnet werden. Heap-Zahlen werden zum Speichern von Werten verwendet, die nicht in das SMI-Format passen, z. B. Doppelwerte, oder wenn ein Wert umgewandelt werden muss, z. B. wenn Eigenschaften für ihn festgelegt werden.

Strings können an folgenden Stellen gespeichert werden:

  • dem VM-Heap oder
  • extern im Arbeitsspeicher des Renderers. Ein Wrapper-Objekt wird erstellt und für den Zugriff auf externen Speicher verwendet, in dem beispielsweise Scriptquellen und andere Inhalte gespeichert werden, die aus dem Web empfangen werden, anstatt sie in den VM-Heap zu kopieren.

Der Arbeitsspeicher für neue JavaScript-Objekte wird aus einem speziellen JavaScript-Heap (oder VM-Heap) zugewiesen. Diese Objekte werden vom Garbage Collector von V8 verwaltet und bleiben daher aktiv, solange mindestens eine starke Referenz darauf besteht.

Native Objekte sind alle anderen Objekte, die sich nicht im JavaScript-Heap befinden. Native Objekte werden im Gegensatz zu Heap-Objekten während ihrer gesamten Lebensdauer nicht vom V8-Garbage Collector verwaltet. Der Zugriff darauf ist nur über das JavaScript-Wrapper-Objekt möglich.

Ein Cons-String ist ein Objekt, das aus Strings besteht, die gespeichert und dann zusammengeführt werden. Er ist das Ergebnis einer Konkatenierung. Die Zusammenführung der Inhalte des cons-Strings erfolgt nur bei Bedarf. Ein Beispiel wäre, wenn ein Teilstring eines zusammengefügten Strings erstellt werden muss.

Wenn Sie beispielsweise a und b zusammenführen, erhalten Sie den String „(a, b)“, der das Ergebnis der Zusammenführung darstellt. Wenn Sie später d mit diesem Ergebnis verketten, erhalten Sie einen weiteren Cons-String ((a, b), d).

Arrays: Ein Array ist ein Objekt mit numerischen Schlüsseln. Sie werden in der V8-VM häufig zum Speichern großer Datenmengen verwendet. Schlüssel/Wert-Paare, die wie Wörterbücher verwendet werden, werden von Arrays unterstützt.

Ein typisches JavaScript-Objekt kann einer von zwei Arraytypen sein, die zum Speichern verwendet werden:

  • benannte Unterkünfte und
  • numerische Elemente

Bei einer sehr geringen Anzahl von Properties können diese intern im JavaScript-Objekt selbst gespeichert werden.

Map: Ein Objekt, das die Art des Objekts und sein Layout beschreibt. Mithilfe von Karten lassen sich beispielsweise implizite Objekthierarchien für den schnellen Zugriff auf Properties beschreiben.

Objektgruppen

Jede Gruppe von nativen Objekten besteht aus Objekten, die gegenseitige Verweise enthalten. Betrachten Sie beispielsweise einen DOM-Unterbaum, in dem jeder Knoten einen Link zu seinem übergeordneten Element und zum nächsten untergeordneten Element und zum nächsten Geschwisterelement hat, wodurch ein zusammenhängendes Diagramm entsteht. Native Objekte sind nicht im JavaScript-Heap vertreten. Deshalb haben sie eine Größe von 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 enthält wiederum Wrapper-Objekte. Dies führt jedoch nicht zu einem nicht wiederverwendbaren Zyklus, da der GC intelligent genug ist, Objektgruppen freizugeben, auf deren Wrapper nicht mehr verwiesen wird. Wenn Sie jedoch einen einzelnen Wrapper nicht veröffentlichen, wird die gesamte Gruppe und die zugehörigen Wrapper zurückgehalten.