Speicherverwaltung in der JavaScript-Engine

Einführung

Dieses Dokument beschreibt die dynamische Speicherverwaltung der JavaScript-Engine in QML. Es ist eine eher technische, tiefgehende Beschreibung. Sie brauchen es nur zu lesen, wenn Sie sich für die genauen Eigenschaften der JavaScript-Speicherverwaltung in QML interessieren. Insbesondere kann es hilfreich sein, wenn Sie versuchen, Ihre Anwendung für maximale Leistung zu optimieren.

Hinweis: Durch Kompilieren Ihres QML-Codes nach C++ unter Verwendung der Qt Quick Compiler können Sie einen Großteil der JavaScript-Heap-Nutzung vermeiden. Der generierte C++-Code verwendet den bekannten C++-Stack und -Heap zum Speichern von Objekten und Werten. Die JavaScript-Host-Umgebung verwendet jedoch immer etwas von JavaScript verwalteten Speicher, unabhängig davon, ob Sie ihn verwenden oder nicht. Wenn Sie Funktionen verwenden, die nicht nach C++ kompiliert werden können, greift die Engine auf die Interpretation oder JIT-Kompilierung zurück und verwendet JavaScript-Objekte, die auf dem JavaScript-Heap gespeichert sind.

Grundprinzipien

Die JavaScript-Engine in QML hat einen eigenen Speichermanager, der Adressraum in Einheiten von mehreren Seiten vom Betriebssystem anfordert. Objekte, Strings und andere verwaltete Werte, die in JavaScript erstellt werden, werden dann in diesem Adressraum unter Verwendung des eigenen Zuweisungsschemas der JavaScript-Engine abgelegt. Die JavaScript-Engine verwendet weder malloc() und free() der C-Bibliothek noch die Standardimplementierungen von new und delete von C++, um Speicher für JavaScript-Objekte zuzuweisen.

Anfragen für Adressraum werden im Allgemeinen mit mmap() auf Unix-ähnlichen Systemen und mit VirtualAlloc() auf Windows durchgeführt. Es gibt mehrere plattformspezifische Implementierungen dieser Primitive. Der auf diese Weise reservierte Adressraum wird nicht sofort in den physischen Speicher übernommen. Vielmehr merkt das Betriebssystem, wenn auf eine Speicherseite tatsächlich zugegriffen wird, und überträgt sie erst dann. Daher ist der Adressraum praktisch frei, und ein großer Teil davon gibt dem JavaScript-Speicherverwalter den nötigen Einfluss, um Objekte effizient auf dem JavaScript-Heap zu platzieren. Darüber hinaus gibt es plattformspezifische Techniken, um dem Betriebssystem mitzuteilen, dass ein Teil des Adressraums zwar noch reserviert ist, aber vorerst nicht in den physischen Speicher eingeordnet werden muss. Das Betriebssystem kann dann die Freigabe des Speichers bei Bedarf aufheben und ihn für andere Aufgaben verwenden. Entscheidend ist, dass die meisten Betriebssysteme keine sofortige Reaktion auf eine solche Freigabeanforderung garantieren. Sie heben die Freigabe des Speichers erst dann auf, wenn er tatsächlich für etwas anderes benötigt wird. Auf Unix-ähnlichen Systemen verwenden wir im Allgemeinen madvise() für diese Aufgabe. Windows hat spezielle Flags für VirtualFree(), um das Gleiche zu tun.

Hinweis: Es gibt Speicher-Profiling-Tools, die diesen Mechanismus nicht verstehen und den JavaScript-Speicherverbrauch zu hoch angeben.

Alle Werte, die auf dem JavaScript-Heap gespeichert sind, unterliegen der Garbage Collection. Keiner der Werte wird sofort "gelöscht", wenn er den Gültigkeitsbereich verlässt oder anderweitig "fallen gelassen" wird. Nur der Garbage Collector kann Werte aus dem JavaScript-Heap entfernen und Speicher zurückgeben (siehe Garbage Collection weiter unten).

QObject-basierte Typen

QObject-basierte Typen und insbesondere alles, was Sie als QML-Element formulieren können, werden auf dem C++-Heap alloziert. Nur ein kleiner Wrapper um den Zeiger wird auf dem JavaScript-Heap abgelegt, wenn von JavaScript aus auf QObject zugegriffen wird. Ein solcher Wrapper kann jedoch das QObject besitzen, auf das er zeigt. Siehe QJSEngine::ObjectOwnership. Wenn der Wrapper Eigentümer des Objekts ist, wird es gelöscht, wenn der Wrapper in den Müll geworfen wird. Sie können die Löschung auch manuell auslösen, indem Sie die Methode destroy() aufrufen. destroy() ruft intern QObject::deleteLater() auf. Daher wird das Objekt nicht sofort gelöscht, sondern es wird auf die nächste Iteration der Ereignisschleife gewartet.

Die in QML deklarierten Eigenschaften von Objekten werden auf dem JavaScript-Heap gespeichert. Sie leben so lange, wie das Objekt, zu dem sie gehören, lebt. Danach werden sie bei der nächsten Ausführung des Garbage Collectors entfernt.

Objekt-Zuweisung

In JavaScript ist jeder strukturierte Typ ein Objekt. Dazu gehören Funktionsobjekte, Arrays, reguläre Ausdrücke, Datumsobjekte und vieles mehr. QML verfügt über eine Reihe interner Objekttypen, wie z. B. den oben erwähnten QObject Wrapper. Immer, wenn ein Objekt erstellt wird, sucht der Speichermanager einen Speicherplatz dafür auf dem JavaScript-Heap.

JavaScript-Zeichenfolgen sind ebenfalls verwaltete Werte, aber ihre Zeichenkettendaten werden nicht auf dem JavaScript-Heap zugewiesen. Ähnlich wie die QObject -Wrapper sind die Heap-Objekte für Strings nur dünne Hüllen um einen Zeiger auf String-Daten.

Bei der Zuweisung von Speicher für ein Objekt wird die Größe des Objekts zunächst auf 32 Byte aufgerundet. Jedes 32-Byte-Stück des Adressraums wird als "Slot" bezeichnet. Bei Objekten, die kleiner sind als ein Schwellenwert für die "riesige Größe", führt der Speichermanager eine Reihe von Versuchen durch, das Objekt im Speicher zu platzieren:

  • Der Speichermanager führt verknüpfte Listen von zuvor freigegebenen Teilen des Heaps, die als "Bins" bezeichnet werden. Jeder Bin enthält Teile des Heaps mit einer festen Größe pro Bin in Slots. Wenn das Fach für die richtige Größe nicht leer ist, wählt er den ersten Eintrag und legt das Objekt dort ab.
  • Der noch nicht genutzte Speicher wird über einen Bumper Allocator verwaltet. Ein Bumper-Zeiger zeigt auf das Byte jenseits des belegten Adressraums. Wenn noch genügend ungenutzter Adressraum vorhanden ist, wird der Bumper entsprechend vergrößert, und das Objekt wird im ungenutzten Raum abgelegt.
  • Für bereits freigegebene Heap-Stücke unterschiedlicher Größe, die größer als die oben genannten spezifischen Größen sind, wird ein separates Verzeichnis geführt. Der Speichermanager durchläuft diese Liste und versucht, ein Stück zu finden, das er aufteilen kann, um das neue Objekt unterzubringen.
  • Der Speichermanager durchsucht die Listen der spezifisch dimensionierten Bins, die größer sind als das zuzuweisende Objekt, und versucht, einen davon aufzuteilen.
  • Wenn nichts davon funktioniert, reserviert der Speichermanager mehr Adressraum und weist das Objekt mit Hilfe des Bumper-Allokators zu.

Riesige Objekte werden von einem eigenen Allokator behandelt. Für jedes dieser Objekte werden eine oder mehrere separate Speicherseiten vom Betriebssystem bezogen und separat verwaltet.

Zusätzlich erhält jedes neue Stück Adressraum, das der Speichermanager vom Betriebssystem erhält, einen Header, der eine Reihe von Flags für jeden Slot enthält:

  • Objekt: Der erste von einem Objekt belegte Steckplatz wird mit diesem Bit gekennzeichnet.
  • erweitert: Alle weiteren von einem Objekt belegten Slots werden mit diesem Bit gekennzeichnet.
  • mark: Wenn der Garbage Collector läuft, setzt er dieses Bit, wenn das Objekt noch in Gebrauch ist.

Interne Klassen

Um den erforderlichen Speicherplatz für Metadaten über die Mitglieder eines Objekts zu minimieren, weist die JavaScript-Engine jedem Objekt eine "interne Klasse" zu. Andere JavaScript-Engines nennen dies "versteckte Klasse" oder "Form". Interne Klassen werden dedupliziert und in einem Baum gespeichert. Wird einem Objekt eine Eigenschaft hinzugefügt, werden die Kinder der aktuellen internen Klasse daraufhin überprüft, ob dasselbe Objektlayout schon einmal vorkam. Wenn dies der Fall ist, können wir die daraus resultierende interne Klasse sofort verwenden. Andernfalls müssen wir eine neue Klasse erstellen.

Interne Klassen werden in einem eigenen Abschnitt des JavaScript-Heap gespeichert, der ansonsten genauso funktioniert wie die oben beschriebene allgemeine Objektzuweisung. Der Grund dafür ist, dass interne Klassen am Leben erhalten werden müssen, während die Objekte, die sie verwenden, gesammelt werden. Interne Klassen werden dann in einem separaten Durchgang gesammelt.

Die eigentlichen Eigenschaftsattribute, die in internen Klassen gespeichert sind, werden jedoch nicht auf dem JavaScript-Heap gehalten, sondern mit new und delete verwaltet.

Garbage Collection

Der in der JavaScript-Engine verwendete Garbage-Collector ist ein nicht beweglicher Mark-and-Sweep-Entwurf mit Stop-the-World. In der Markierungsphase werden alle bekannten Orte durchlaufen, an denen sich lebende Verweise auf Objekte befinden können. Dies sind insbesondere:

  • JavaScript-Globale
  • Nicht löschbare Teile von QML- und JavaScript-Kompiliereinheiten
  • Der JavaScript-Stapel
  • Der Speicher für persistente Werte. Hier speichern QJSValue und ähnliche Klassen Verweise auf JavaScript-Objekte.

Für jedes Objekt, das an diesen Stellen gefunden wird, werden die Markierungsbits für alle Verweise rekursiv gesetzt.

In der Sweep-Phase durchläuft der Garbage Collector dann den gesamten Heap und gibt alle Objekte frei, die zuvor nicht markiert wurden. Der so freigegebene Speicher wird in die Bins sortiert, die für weitere Zuweisungen verwendet werden. Wenn ein Teil des Adressraums völlig leer ist, wird er freigegeben, der Adressraum bleibt jedoch erhalten (siehe Grundprinzipien oben). Wächst der Speicherbedarf wieder an, wird derselbe Adressraum erneut verwendet.

Der Garbage Collector wird entweder manuell durch Aufruf der Funktion gc() oder durch eine Heuristik ausgelöst, die die folgenden Aspekte berücksichtigt:

  • Die Menge an Speicher, die von Objekten auf dem JavaScript-Heap verwaltet wird, aber nicht direkt auf dem JavaScript-Heap zugewiesen ist, wie z. B. Strings und interne Klassendaten. Für diese wird ein dynamischer Schwellenwert beibehalten. Wenn dieser überschritten wird, wird der Garbage Collector aktiviert und der Schwellenwert erhöht. Wenn die Menge des verwalteten externen Speichers weit unter den Schwellenwert fällt, wird der Schwellenwert gesenkt.
  • Der gesamte reservierte Adressraum. Die interne Speicherzuweisung auf dem JavaScript-Heap wird erst dann berücksichtigt, wenn zumindest ein Teil des Adressraums reserviert wurde.
  • Die zusätzliche Adressraumreservierung seit dem letzten Lauf des Garbage Collectors. Wenn die Menge des Adressraums mehr als doppelt so groß ist wie die Menge des verwendeten Speichers nach dem letzten Garbage-Collector-Lauf, wird der Garbage-Collector erneut ausgeführt.

Analyse des Speicherverbrauchs

Um die Entwicklung sowohl des Adressraums als auch der Anzahl der darin allokierten Objekte zu beobachten, ist es am besten, ein spezialisiertes Tool zu verwenden. Das Programm QML Profiler bietet eine Visualisierung, die dabei hilft. Allgemeinere Tools können nicht sehen, was der JavaScript-Speichermanager in dem von ihm reservierten Adressraum tut, und bemerken möglicherweise nicht einmal, dass ein Teil des Adressraums nicht im physischen Speicher belegt ist.

Eine weitere Möglichkeit, die Speichernutzung zu debuggen, sind die logging categories qt.qml.gc.statistics und qt.qml.gc.allocatorStats. Wenn Sie den Debug-Level für qt.qml.gc.statistics aktivieren, wird der Garbage Collector bei jedem Lauf einige Informationen ausgeben:

  • Wie viel Adressraum insgesamt reserviert ist
  • Wie viel Speicher vor und nach der Garbage Collection verwendet wurde
  • Wie viele Objekte unterschiedlicher Größe bisher alloziert wurden

Die Debug-Stufe für qt.qml.gc.allocatorStats gibt detailliertere Statistiken aus, die auch beinhalten, wie der Garbage Collector ausgelöst wurde, Zeitangaben für die Markierungs- und Sweep-Phasen und eine detaillierte Aufschlüsselung der Speichernutzung nach Bytes und Adressraumabschnitten.

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.