Einführung
In JavaScript wird zwar die Speicherbereinigung für die automatische Arbeitsspeicherverwaltung verwendet, sie ist jedoch kein Ersatz für eine effektive Arbeitsspeicherverwaltung in Anwendungen. JavaScript-Anwendungen leiden unter denselben speicherbezogenen Problemen wie native Anwendungen, z. B. Speicherlecks und Speicherausweitung. Außerdem müssen sie mit Pausen bei der Garbage Collection umgehen. Bei großen Anwendungen wie Gmail treten dieselben Probleme auf wie bei kleineren Anwendungen. Im Folgenden erfahren Sie, wie das Gmail-Team mithilfe der Chrome-Entwicklertools seine Speicherprobleme identifiziert, isoliert und behoben hat.
Google I/O 2013-Sitzung
Wir haben diese Informationen auf der Google I/O 2013 vorgestellt. Sehen Sie sich das Video unten an:
Gmail, wir haben ein Problem…
Das Gmail-Team stand vor einem ernsten Problem. Es gab immer mehr Berichte über Gmail-Tabs, die auf ressourcenarmen Laptops und Desktop-Computern mehrere Gigabyte Arbeitsspeicher verbrauchen und häufig zum Absturz des gesamten Browsers führen. Geschichten über CPUs, die zu 100 % ausgelastet sind, Apps, die nicht reagieren, und traurige Chrome-Tabs („Er ist tot, Jim“). Das Team wusste nicht, wie es mit der Diagnose des Problems beginnen sollte, geschweige denn, wie es behoben werden könnte. Sie hatten keine Ahnung, wie weit verbreitet das Problem war, und die verfügbaren Tools konnten nicht für große Anwendungen skaliert werden. Das Team hat sich mit den Chrome-Teams zusammengetan und gemeinsam neue Methoden zur Triage von Speicherproblemen entwickelt, bestehende Tools verbessert und die Erhebung von Speicherdaten aus dem Feld ermöglicht. Bevor wir uns jedoch den Tools zuwenden, sehen wir uns die Grundlagen der JavaScript-Speicherverwaltung an.
Grundlagen der Speicherverwaltung
Bevor Sie den Arbeitsspeicher in JavaScript effektiv verwalten können, müssen Sie die Grundlagen kennen. In diesem Abschnitt werden primitive Typen, die Objektgrafik und Definitionen für Speicherauslastung im Allgemeinen und Speicherlecks in JavaScript behandelt. Der Speicher in JavaScript kann als Graph dargestellt werden. Daher spielt die Graphentheorie eine Rolle bei der JavaScript-Speicherverwaltung und dem Heap-Profiler.
Einfache Typen
JavaScript hat drei primitive Typen:
- Zahl (z. B. 4, 3,14159)
- Boolesch (wahr oder falsch)
- String („Hallo Welt“)
Auf diese primitiven Typen können keine anderen Werte verweisen. In der Objektgrafik sind diese Werte immer Endknoten, d. h., sie haben nie eine ausgehende Kante.
Es gibt nur einen Containertyp: das Objekt. In JavaScript ist das Objekt ein assoziatives Array. Ein nicht leeres Objekt ist ein innerer Knoten mit ausgehenden Kanten zu anderen Werten (Knoten).
Was ist mit Arrays?
Ein Array in JavaScript ist eigentlich ein Objekt mit numerischen Schlüsseln. Das ist eine Vereinfachung, da JavaScript-Laufzeitumgebungen arraysähnliche Objekte optimieren und sie intern als Arrays darstellen.
Terminologie
- Wert: Eine Instanz eines primitiven Typs, eines Objekts, eines Arrays usw.
- Variable: Ein Name, der auf einen Wert verweist.
- Property: Ein Name in einem Objekt, der auf einen Wert verweist.
Objektgraph
Alle Werte in JavaScript sind Teil des Objektgraphs. Der Graph beginnt mit Wurzeln, z. B. dem Fensterobjekt. Sie können die Lebensdauer von GC-Roots nicht verwalten, da sie vom Browser erstellt und beim Entladen der Seite gelöscht werden. Globale Variablen sind eigentlich Eigenschaften des Fensters.
Wann wird ein Wert zu einem Junk-Wert?
Ein Wert wird zu einem Junk-Wert, wenn es keinen Pfad von einem Stammknoten zum Wert gibt. Mit anderen Worten: Wenn bei der Suche nach einem Wert alle Objekteigenschaften und ‑variablen im Stackframe abgefragt werden, kann er nicht gefunden werden, da er zu einem Garbage-Wert geworden ist.
Was ist ein Speicherleck in JavaScript?
Ein Speicherleck in JavaScript tritt am häufigsten auf, wenn es DOM-Knoten gibt, die nicht über den DOM-Baum der Seite erreichbar sind, aber trotzdem von einem JavaScript-Objekt referenziert werden. Moderne Browser machen es zwar immer schwieriger, versehentlich Datenlecks zu verursachen, aber es ist immer noch einfacher, als man denkt. Angenommen, Sie hängen dem DOM-Baum ein Element so an:
email.message = document.createElement("div");
displayList.appendChild(email.message);
Später entfernen Sie das Element aus der Anzeigeliste:
displayList.removeAllChildren();
Solange email
vorhanden ist, wird das DOM-Element, auf das in der Nachricht verwiesen wird, nicht entfernt, auch wenn es jetzt vom DOM-Baum der Seite getrennt ist.
Was ist Bloat?
Ihre Seite ist überladen, wenn Sie mehr Speicherplatz verbrauchen, als für eine optimale Seitengeschwindigkeit erforderlich ist. Indirekt führen auch Speicherlecks zu einer Vergrößerung des Speicherbedarfs, was aber nicht beabsichtigt ist. Ein Anwendungscache ohne Größenbeschränkung ist eine häufige Ursache für einen Speicheranstieg. Außerdem kann Ihre Seite durch Hostdaten aufgebläht sein, z. B. Pixeldaten, die aus Bildern geladen werden.
Was ist die automatische Speicherbereinigung?
Die automatische Speicherbereinigung ist die Methode, mit der in JavaScript Speicher zurückgewonnen wird. Wann dies geschieht, entscheidet der Browser. Während einer Datenerhebung wird die gesamte Scriptausführung auf Ihrer Seite ausgesetzt, während Livewerte durch Durchlaufen der Objektgrafik beginnend bei den GC-Wurzeln ermittelt werden. Alle Werte, die nicht erreichbar sind, werden als Junk klassifiziert. Der Arbeitsspeicher für Junk-Werte wird vom Speichermanager wiederverwendet.
V8-Speicherbereinigung im Detail
Um die Funktionsweise der Garbage Collection besser zu verstehen, sehen wir uns den V8-Garbage Collector genauer an. V8 verwendet einen Generations-Collector. Das Gedächtnis wird in zwei Generationen unterteilt: das junge und das alte Gedächtnis. Die Zuweisung und Erhebung bei der jungen Generation erfolgt schnell und häufig. Die Zuweisung und Erhebung innerhalb der alten Generation ist langsamer und seltener.
Generational Collector
V8 verwendet einen Collector der zweiten Generation. Das Alter eines Werts wird als Anzahl der seit der Zuweisung zugewiesenen Byte definiert. In der Praxis wird das Alter eines Werts oft anhand der Anzahl der Sammlungen der jüngeren Generation geschätzt, die er überlebt hat. Wenn ein Wert älter als die angegebene Zeit ist, wird er in die alte Generation verschoben.
In der Praxis haben neu zugewiesene Werte nur eine kurze Lebensdauer. Eine Studie zu Smalltalk-Programmen hat gezeigt, dass nur 7% der Werte nach einer Sammlung der jungen Generation übrig bleiben. Ähnliche Studien für verschiedene Laufzeiten haben ergeben, dass durchschnittlich zwischen 90% und 70% der neu zugewiesenen Werte nie in die alte Generation übernommen werden.
Young Generation
Der Heap der jungen Generation in V8 ist in zwei Bereiche aufgeteilt, die „from“ und „to“ heißen. Der Arbeitsspeicher wird aus dem To-Bereich zugewiesen. Die Zuordnung erfolgt sehr schnell, bis der Speicherplatz voll ist. In diesem Fall wird eine Sammlung der jungen Generation ausgelöst. Bei der Sammlung der jüngeren Generation werden zuerst der „From-Space“ und der „To-Space“ getauscht. Der alte „To-Space“ (jetzt der „From-Space“) wird gescannt und alle aktuellen Werte werden in den „To-Space“ kopiert oder in die alte Generation verschoben. Eine typische Sammlung der jungen Generation dauert etwa 10 Millisekunden (ms).
Intuitiv sollten Sie verstehen, dass Sie mit jeder Zuweisung Ihrer Anwendung dem Erreichen des Speicherplatzes näher kommen und eine GC-Pause verursachen. Hinweis für Spieleentwickler: Um eine Framezeit von 16 ms zu erreichen (erforderlich, um 60 Frames pro Sekunde zu erzielen), darf Ihre Anwendung keine Zuweisungen vornehmen, da eine einzelne Sammlung der jungen Generation den Großteil der Framezeit in Anspruch nimmt.
Alte Generation
Für den Heap der alten Generation in V8 wird zur Sammlung ein Mark-Compact-Algorithmus verwendet. Zuweisungen der alten Generation erfolgen immer dann, wenn ein Wert aus der jungen Generation in die alte Generation verschoben wird. Jedes Mal, wenn eine Sammlung der alten Generation erfolgt, wird auch eine Sammlung der jungen Generation durchgeführt. Ihre Anwendung wird für einige Sekunden pausiert. In der Praxis ist das akzeptabel, da Sammlungen der alten Generation selten sind.
V8-GC-Zusammenfassung
Die automatische Speicherverwaltung mit Garbage Collection ist zwar eine gute Möglichkeit, die Produktivität von Entwicklern zu steigern, aber jedes Mal, wenn Sie einen Wert zuweisen, nähern Sie sich einer Pause bei der Garbage Collection. Pausen bei der Speicherbereinigung können das Erscheinungsbild Ihrer Anwendung beeinträchtigen, da es zu Rucklern kommt. Da Sie jetzt wissen, wie JavaScript den Arbeitsspeicher verwaltet, können Sie die richtigen Entscheidungen für Ihre Anwendung treffen.
Gmail beheben
Im letzten Jahr wurden zahlreiche Funktionen und Fehlerkorrekturen in die Chrome-Entwicklertools aufgenommen, wodurch sie leistungsfähiger denn je sind. Außerdem wurde im Browser selbst eine wichtige Änderung an der performance.memory API vorgenommen, die es Gmail und anderen Anwendungen ermöglicht, Speicherstatistiken aus dem Feld zu erfassen. Mit diesen tollen Tools wurde das, was einst wie eine unmögliche Aufgabe erschien, bald zu einem spannenden Spiel, bei dem es darum ging, die Täter zu finden.
Tools und Techniken
Felddaten und performance.memory API
Ab Chrome 22 ist die performance.memory API standardmäßig aktiviert. Bei lang laufenden Anwendungen wie Gmail sind Daten von echten Nutzern von unschätzbarem Wert. Anhand dieser Informationen können wir zwischen Powernutzern – also Nutzern, die 8 bis 16 Stunden pro Tag in Gmail verbringen und täglich Hunderte von E-Mails erhalten – und durchschnittlichen Nutzern unterscheiden, die täglich nur wenige Minuten in Gmail verbringen und etwa ein Dutzend E-Mails pro Woche erhalten.
Diese API gibt drei Daten zurück:
- jsHeapSizeLimit – Die Größe des Arbeitsspeichers (in Byte), auf die der JavaScript-Heap begrenzt ist.
- totalJSHeapSize: Die Größe des Arbeitsspeichers (in Byte), die dem JavaScript-Heap zugewiesen wurde, einschließlich des freien Speicherplatzes.
- usedJSHeapSize: Die derzeit verwendete Arbeitsspeichermenge (in Byte).
Die API gibt Speicherwerte für den gesamten Chrome-Prozess zurück. Obwohl dies nicht der Standardmodus ist, kann Chrome unter bestimmten Umständen mehrere Tabs im selben Renderer-Prozess öffnen. Das bedeutet, dass die von „performance.memory“ zurückgegebenen Werte neben dem Arbeitsspeicherbedarf des Tabs mit Ihrer App auch den Arbeitsspeicherbedarf anderer Browser-Tabs enthalten können.
Arbeitsspeicher in großem Umfang messen
Gmail hat sein JavaScript so instrumentiert, dass die performance.memory API verwendet wird, um etwa alle 30 Minuten Speicherinformationen zu erfassen. Da viele Gmail-Nutzer die App tagelang geöffnet lassen, konnte das Team die Speichernutzung im Zeitverlauf sowie Statistiken zur Gesamtspeicherbelegung erfassen. Innerhalb weniger Tage nach der Instrumentierung von Gmail zur Erhebung von Speicherinformationen von einer zufälligen Stichprobe von Nutzern hatte das Team genügend Daten, um zu verstehen, wie weit verbreitet die Speicherprobleme bei durchschnittlichen Nutzern waren. Er hat einen Referenzwert festgelegt und anhand des Streams eingehender Daten den Fortschritt bei der Reduzierung des Speicherverbrauchs verfolgt. Diese Daten werden auch verwendet, um Rückfälle bei der Arbeitsspeichernutzung zu erkennen.
Neben den Tracking-Zwecken bieten die Feldmessungen auch einen guten Einblick in die Korrelation zwischen Arbeitsspeicherbedarf und Anwendungsleistung. Entgegen der weit verbreiteten Meinung, dass „mehr Arbeitsspeicher zu einer besseren Leistung führt“, stellte das Gmail-Team fest, dass die Latenz bei gängigen Gmail-Aktionen umso länger ist, je größer der Arbeitsspeicherbedarf ist. Mit dieser Erkenntnis war das Team motivierter denn je, den Arbeitsspeicherverbrauch einzudämmen.
Arbeitsspeicherproblem mit der Zeitachse in den DevTools identifizieren
Der erste Schritt zur Lösung eines Leistungsproblems besteht darin, nachzuweisen, dass das Problem tatsächlich besteht, einen reproduzierbaren Test zu erstellen und eine Baseline-Messung des Problems durchzuführen. Ohne ein reproduzierbares Programm können Sie das Problem nicht zuverlässig messen. Ohne Baseline-Messung wissen Sie nicht, inwiefern Sie die Leistung verbessert haben.
Der Zeitleistenbereich in den DevTools eignet sich hervorragend, um nachzuweisen, dass das Problem tatsächlich besteht. Sie erhalten einen vollständigen Überblick darüber, wie viel Zeit beim Laden und Interagieren mit Ihrer Webanwendung oder -seite in Anspruch genommen wird. Alle Ereignisse, vom Laden von Ressourcen über das Parsen von JavaScript, das Berechnen von Stilen, Pausen bei der Garbage Collection bis hin zum Neuzeichnen, werden in einer Zeitachse dargestellt. Zum Untersuchen von Speicherproblemen gibt es im Zeitleistenbereich auch einen Speichermodus, in dem der zugewiesene Gesamtspeicher, die Anzahl der DOM-Knoten, die Anzahl der Fensterobjekte und die Anzahl der zugewiesenen Ereignis-Listener erfasst werden.
Nachweis eines Problems
Identifizieren Sie zuerst eine Abfolge von Aktionen, bei denen Sie vermuten, dass ein Speicherleck auftritt. Starten Sie die Aufzeichnung der Zeitachse und führen Sie die Abfolge der Aktionen aus. Mit der Papierkorbschaltfläche unten können Sie eine vollständige Garbage Collection erzwingen. Wenn Sie nach einigen Iterationen ein Sägezahnmuster sehen, weisen Sie zu viele kurzlebige Objekte zu. Wenn die Abfolge der Aktionen jedoch nicht zu einem Speicherverbrauch führen sollte und die Anzahl der DOM-Knoten nicht wieder auf den Ausgangswert zurückgeht, haben Sie guten Grund zu vermuten, dass ein Speicherleck vorliegt.
Sobald Sie bestätigt haben, dass das Problem besteht, können Sie mit dem DevTools-Heap-Profiler die Ursache ermitteln.
Speicherlecks mit dem Heap-Profiler in den DevTools finden
Im Bereich „Profiler“ finden Sie sowohl einen CPU- als auch einen Heap-Profiler. Beim Heap-Profiling wird ein Snapshot der Objektgrafik erstellt. Bevor ein Snapshot erstellt wird, werden sowohl die neue als auch die alte Generation beseitigt. Mit anderen Worten: Sie sehen nur Werte, die zum Zeitpunkt der Aufnahme des Snapshots aktiv waren.
Der Heap-Profiler bietet zu viele Funktionen, um sie in diesem Artikel ausreichend zu behandeln. Eine ausführliche Dokumentation finden Sie jedoch auf der Website für Chrome-Entwickler. Wir konzentrieren uns hier auf den Heap-Zuweisungs-Profiler.
Heap-Zuweisungs-Profiler verwenden
Der Heap-Zuweisungs-Profiler kombiniert die detaillierten Snapshot-Informationen des Heap-Profilers mit der inkrementellen Aktualisierung und dem Tracking des Zeitachsenbereichs. Öffnen Sie den Bereich „Profile“, starten Sie ein Profil vom Typ Heap-Zuweisungen erfassen, führen Sie eine Abfolge von Aktionen aus und beenden Sie die Aufzeichnung zur Analyse. Der Allokations-Profiler erstellt während der Aufzeichnung regelmäßig Heap-Snapshots (bis zu alle 50 ms) und einen abschließenden Snapshot am Ende der Aufzeichnung.
Die Balken oben geben an, wann neue Objekte im Heap gefunden werden. Die Höhe der einzelnen Balken entspricht der Größe der kürzlich zugewiesenen Objekte. Die Farbe der Balken gibt an, ob diese Objekte im finalen Heap-Snapshot noch aktiv sind: Blaue Balken stehen für Objekte, die am Ende der Zeitachse noch aktiv sind. Graue Balken stehen für Objekte, die während der Zeitachse zugewiesen wurden, aber inzwischen durch die Garbage Collection entfernt wurden.
Im Beispiel oben wurde eine Aktion zehnmal ausgeführt. Das Beispielprogramm speichert fünf Objekte im Cache. Daher sind die letzten fünf blauen Balken zu erwarten. Der linkeste blaue Balken weist jedoch auf ein potenzielles Problem hin. Mit den Schiebereglern in der Zeitleiste oben können Sie dann diesen bestimmten Snapshot heranzoomen und sich die Objekte ansehen, die zu diesem Zeitpunkt kürzlich zugewiesen wurden. Wenn Sie auf ein bestimmtes Objekt im Heap klicken, wird im unteren Bereich des Heap-Snapshots der zugehörige Baum angezeigt. Wenn Sie den Speicherpfad zum Objekt untersuchen, sollten Sie genügend Informationen erhalten, um zu verstehen, warum das Objekt nicht erfasst wurde. Sie können dann die erforderlichen Codeänderungen vornehmen, um die unnötige Referenz zu entfernen.
Speicherprobleme in Gmail beheben
Mithilfe der oben beschriebenen Tools und Techniken konnte das Gmail-Team einige Kategorien von Fehlern identifizieren: unbegrenzte Caches, unendlich wachsende Arrays von Rückrufen, die auf etwas warten, das nie passiert, und Ereignis-Listener, die ihre Ziele versehentlich beibehalten. Durch die Behebung dieser Probleme konnte die Gesamtspeichernutzung von Gmail drastisch reduziert werden. Nutzer im 99. Perzentil benötigten 80 % weniger Speicher als zuvor und der Speicherverbrauch der Mediannutzer sank um fast 50 %.
Da Gmail weniger Speicherplatz benötigt, wurde die GC-Pausenlatenz reduziert, was die Nutzerfreundlichkeit insgesamt verbesserte.
Außerdem konnte das Gmail-Team durch das Erfassen von Statistiken zur Speichernutzung Rückschritte bei der Garbage Collection in Chrome feststellen. Insbesondere wurden zwei Fragmentierungsfehler entdeckt, als die Speicherdaten von Gmail einen dramatischen Anstieg der Lücke zwischen dem zugewiesenen Gesamtspeicher und dem Arbeitsspeicher zeigten.
Call-to-Action
Stellen Sie sich folgende Fragen:
- Wie viel Arbeitsspeicher wird von meiner App belegt? Möglicherweise verwenden Sie zu viel Arbeitsspeicher, was sich entgegen der landläufigen Meinung negativ auf die Gesamtleistung der Anwendung auswirkt. Es ist schwierig, die richtige Anzahl zu ermitteln. Prüfen Sie jedoch, ob das zusätzliche Caching auf Ihrer Seite eine messbare Leistungsauswirkung hat.
- Ist meine Seite frei von Leaks? Wenn Ihre Seite Speicherlecks aufweist, kann sich das nicht nur auf die Leistung der Seite, sondern auch auf andere Tabs auswirken. Mit dem Objekt-Tracker können Sie Lecks eingrenzen.
- Wie oft wird meine Seite vom Garbage Collector beseitigt? Sie können alle GC-Pausen im Zeitachsenbereich in den Chrome-Entwicklertools sehen. Wenn die Garbage Collection für Ihre Seite häufig ausgeführt wird, ist die Wahrscheinlichkeit hoch, dass Sie zu häufig Speicher zuweisen und den Speicher der jüngeren Generation durchlaufen.
Fazit
Wir haben in einer Krise angefangen. Die wichtigsten Grundlagen der Speicherverwaltung in JavaScript und insbesondere in V8 wurden behandelt. Sie haben gelernt, wie Sie die Tools verwenden, einschließlich der neuen Objekt-Tracker-Funktion, die in den neuesten Chrome-Builds verfügbar ist. Mit diesem Wissen konnte das Gmail-Team das Problem mit der Arbeitsspeichernutzung beheben und die Leistung verbessern. Das können Sie auch mit Ihren Webanwendungen tun.