Auf dieser Seite

Speicherverwaltung in der JavaScript-Engine

Einleitung

Dieses Dokument beschreibt die dynamische Speicherverwaltung der JavaScript-Engine in QML. Es handelt sich um eine eher technische, ausführliche Beschreibung. Sie müssen dies nur 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: Indem Sie Ihren QML-Code mit dem „Qt Quick “-Compiler in C++ kompilieren, können Sie einen Großteil der JavaScript-Heap-Nutzung vermeiden. Der generierte C++-Code nutzt den bekannten C++-Stack und -Heap zum Speichern von Objekten und Werten. Die JavaScript-Host-Umgebung nutzt jedoch immer einen Teil des von JavaScript verwalteten Speichers, unabhängig davon, ob Sie diese Funktionen nutzen oder nicht. Wenn Sie Funktionen nutzen, die nicht in C++ kompiliert werden können, greift die Engine jedoch 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 verfügt über einen eigenen Speichermanager, der Adressraum in Einheiten von mehreren Seiten vom Betriebssystem anfordert. Objekte, Zeichenketten und andere in JavaScript erstellte verwaltete Werte werden dann unter Verwendung des eigenen Zuweisungsschemas der JavaScript-Engine in diesem Adressraum abgelegt. Die JavaScript-Engine verwendet weder die Funktionen `malloc()` und `free()` der C-Bibliothek noch die Standardimplementierungen von `new` und `delete` in C++, um Speicher für JavaScript-Objekte zuzuweisen.

Anforderungen an den Adressraum erfolgen in der Regel mit `mmap()` auf Unix-ähnlichen Systemen und mit `VirtualAlloc()` unter Windows. Es gibt mehrere plattformspezifische Implementierungen dieser Primitive. Auf diese Weise reservierter Adressraum wird nicht sofort im physischen Speicher festgeschrieben. Vielmehr erkennt das Betriebssystem erst, wenn tatsächlich auf eine Speicherseite zugegriffen wird, und schreibt sie erst dann fest. Daher ist der Adressraum praktisch kostenlos, und ein großer Adressraum verschafft dem JavaScript-Speichermanager den nötigen Spielraum, 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, obwohl er noch reserviert ist, vorerst nicht in den physischen Speicher abgebildet werden muss. Das Betriebssystem kann den Speicher dann bei Bedarf freigeben und für andere Aufgaben nutzen. Entscheidend ist, dass die meisten Betriebssysteme keine sofortige Reaktion auf eine solche Freigabeanforderung garantieren. Sie geben den Speicher erst dann frei, wenn er tatsächlich für etwas anderes benötigt wird. Auf Unix-ähnlichen Systemen verwenden wir hierfür in der Regel madvise(). Windows verfügt über spezielle Flags für `VirtualFree()`, um das Gleiche zu erreichen.

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

Alle im JavaScript-Heap gespeicherten Werte unterliegen der Garbage Collection. Keiner der Werte wird sofort „gelöscht“, wenn er den Gültigkeitsbereich verlässt oder anderweitig „freigegeben“ wird. Nur der Garbage Collector darf Werte aus dem JavaScript-Heap entfernen und Speicher freigeben (siehe „Garbage Collection“ weiter unten, um zu erfahren, wie dies funktioniert).

QObject-basierte Typen

QObject-basierte Typen und insbesondere alles, was sich als QML-Element formulieren lässt, werden auf dem C++-Heap zugewiesen. Nur ein kleiner Wrapper um den Zeiger wird auf dem JavaScript-Heap abgelegt, wenn von JavaScript aus auf ein „ QObject “ zugegriffen wird. Ein solcher Wrapper kann jedoch Eigentümer des „ QObject “ sein, auf das er verweist. Siehe QJSEngine::ObjectOwnership. Wenn der Wrapper das Objekt besitzt, wird es gelöscht, sobald der Wrapper von der Garbage Collection entfernt wird. Sie können die Löschung auch manuell auslösen, indem Sie die Methode `destroy()` darauf aufrufen. `destroy()` ruft intern ` QObject::deleteLater()` auf. Das Objekt wird daher nicht sofort gelöscht, sondern es wird auf die nächste Iteration der Ereignisschleife gewartet.

In QML deklarierte Eigenschaften von Objekten werden im JavaScript-Heap gespeichert. Sie bestehen so lange, wie das Objekt, zu dem sie gehören, existiert. Danach werden sie beim nächsten Lauf des Garbage Collectors entfernt.

Objektzuweisung

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 beispielsweise den oben erwähnten „ QObject “-Wrapper. Immer wenn ein Objekt erstellt wird, reserviert der Speichermanager dafür Speicherplatz auf dem JavaScript-Heap.

JavaScript-Zeichenketten sind ebenfalls verwaltete Werte, doch ihre Zeichenkettendaten werden nicht auf dem JavaScript-Heap zugewiesen. Ähnlich wie bei den „ QObject “-Wrappern sind die Heap-Objekte für Zeichenketten lediglich dünne Wrapper um einen Zeiger auf die Zeichenkettendaten.

Bei der Zuweisung von Speicher für ein Objekt wird die Größe des Objekts zunächst auf eine 32-Byte-Ausrichtung aufgerundet. Jeder 32-Byte-Abschnitt des Adressraums wird als „Slot“ bezeichnet. Bei Objekten, die kleiner als ein Schwellenwert für „große Objekte“ sind, unternimmt der Speichermanager eine Reihe von Versuchen, das Objekt im Speicher unterzubringen:

  • Der Speichermanager verwaltet verkettete Listen zuvor freigegebener Heap-Abschnitte, die als „Bins“ bezeichnet werden. Jeder Bin enthält Heap-Abschnitte mit einer festen Größe pro Bin in Slots. Ist der Bin mit der passenden Größe nicht leer, wählt er den ersten Eintrag aus und platziert das Objekt dort.
  • Der noch nicht genutzte Speicher wird über einen „Bumper“-Allokator verwaltet. Ein „Bumper“-Zeiger verweist 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 Bereich abgelegt.
  • Für zuvor freigegebene Heap-Teile unterschiedlicher Größe, die größer sind als die oben genannten spezifischen Größen, wird ein separater Bin vorgehalten. Der Speichermanager durchläuft diese Liste und versucht, einen Teil zu finden, den er aufteilen kann, um das neue Objekt unterzubringen.
  • Der Speichermanager durchsucht die Listen der Bins mit festgelegten Größen, die größer sind als das zuzuweisende Objekt, und versucht, einen dieser Bins zu teilen.
  • Wenn schließlich keine der oben genannten Möglichkeiten funktioniert, reserviert der Speichermanager mehr Adressraum und weist dem Objekt mithilfe des „Bumper“-Allokators Speicher zu.

Sehr große Objekte werden von einem eigenen Allokator verwaltet. Für jedes dieser Objekte werden eine oder mehrere separate Speicherseiten vom Betriebssystem bezogen und separat verwaltet.

Zudem erhält jeder neue Adressraumblock, den der Speichermanager vom Betriebssystem bezieht, einen Header, der für jeden Slot eine Reihe von Flags enthält:

  • object: Der erste von einem Objekt belegte Slot wird mit diesem Bit markiert.
  • extends: Alle weiteren Slots, die von einem Objekt belegt sind, werden mit diesem Bit markiert.
  • mark: Wenn der Garbage Collector läuft, setzt er dieses Bit, sofern das Objekt noch in Gebrauch ist.

Interne Klassen

Um den Speicherbedarf für Metadaten darüber, welche Mitglieder ein Objekt enthält, zu minimieren, weist die JavaScript-Engine jedem Objekt eine „interne Klasse“ zu. Andere JavaScript-Engines bezeichnen dies als „versteckte Klasse“ oder „Shape“. Interne Klassen werden dedupliziert und in einer Baumstruktur gespeichert. Wird einem Objekt eine Eigenschaft hinzugefügt, werden die untergeordneten Elemente der aktuellen internen Klasse überprüft, um festzustellen, ob dasselbe Objektlayout bereits zuvor aufgetreten ist. Ist dies der Fall, kann die resultierende interne Klasse sofort verwendet werden. Andernfalls muss eine neue erstellt werden.

Interne Klassen werden in einem eigenen Bereich des JavaScript-Heaps gespeichert, der ansonsten genauso funktioniert wie die oben beschriebene allgemeine Objektzuweisung. Das liegt daran, dass interne Klassen am Leben erhalten bleiben müssen, während die Objekte, die sie verwenden, gesammelt werden. Interne Klassen werden dann in einem separaten Durchlauf gesammelt.

Die eigentlichen Eigenschaftsattribute, die in internen Klassen gespeichert sind, werden jedoch nicht im JavaScript-Heap gehalten, sondern mithilfe von „new“ und „delete“ verwaltet.

Garbage Collection

Der in der JavaScript-Engine verwendete Garbage Collector basiert auf einem „Mark-and-Sweep“-Design ohne Verschiebung. Seit Qt 6.8 läuft er standardmäßig inkrementell (sofern QV4_GC_TIMELIMIT nicht auf 0 gesetzt ist). In der Markierungsphase werden alle bekannten Stellen durchlaufen, an denen aktive Verweise auf Objekte zu finden sind. Insbesondere:

  • JavaScript-Globals
  • Nicht löschbare Teile von QML- und JavaScript-Kompilierungseinheiten
  • Der JavaScript-Stack
  • Der persistente Wertespeicher. Hier speichern „ QJSValue “ und ähnliche Klassen Verweise auf JavaScript-Objekte.

Für jedes an diesen Stellen gefundene Objekt werden die Markierungsbits rekursiv für alle Objekte gesetzt, auf die es verweist.

In der Sweep-Phase durchläuft der Garbage Collector dann den gesamten Heap und gibt alle zuvor nicht markierten Objekte frei. Der dadurch freigegebene Speicher wird in die Bins sortiert, um für weitere Zuweisungen genutzt zu werden. Ist ein Adressbereich vollständig leer, wird er freigegeben, der Adressraum bleibt jedoch erhalten (siehe „Grundprinzipien“ oben). Steigt der Speicherbedarf wieder an, wird derselbe Adressraum erneut verwendet.

Der Garbage Collector wird entweder manuell durch den Aufruf der Funktion ` gc()` oder durch eine Heuristik ausgelöst, die folgende 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 Klassenmitgliedsdaten. Für diese wird ein dynamischer Schwellenwert gepflegt. Wird dieser überschritten, wird der Garbage Collector ausgeführt und der Schwellenwert erhöht. Fällt die Menge des verwalteten externen Speichers deutlich unter den Schwellenwert, wird dieser gesenkt.
  • Der insgesamt reservierte Adressraum. Die interne Speicherzuweisung auf dem JavaScript-Heap wird erst berücksichtigt, nachdem zumindest ein gewisser Adressraum reserviert wurde.
  • Die zusätzliche Reservierung von Adressraum seit dem letzten Lauf des Garbage Collectors. Wenn die Größe des Adressraums mehr als das Doppelte des nach dem letzten Lauf des Garbage Collectors genutzten Speichers beträgt, führen wir den Garbage Collector erneut aus.

Analyse der Speichernutzung

Um die Entwicklung sowohl des Adressraums als auch der Anzahl der darin zugewiesenen Objekte zu beobachten, empfiehlt sich der Einsatz eines spezialisierten Tools. Das QML Profiler bietet hierfür eine Visualisierung, die dabei hilft. Allgemeinere Tools können nicht erkennen, was der JavaScript-Speichermanager innerhalb des von ihm reservierten Adressraums tut, und bemerken möglicherweise nicht einmal, dass ein Teil des Adressraums nicht im physischen Speicher belegt ist.

Eine weitere Möglichkeit, die Speicherauslastung zu debuggen, bieten die „ logging categories “ qt.qml.gc.statistics und qt.qml.gc.allocatorStats. Wenn Sie für qt.qml.gc.statistics die Debug-Stufe aktivieren, gibt der Garbage Collector bei jedem Durchlauf einige Informationen aus:

  • Wie viel Gesamtadressraum reserviert ist
  • Wie viel Speicher vor und nach der Garbage Collection belegt war
  • Wie viele Objekte unterschiedlicher Größe bisher zugewiesen wurden

Die Debug-Stufe für „qt.qml.gc.allocatorStats“ gibt detailliertere Statistiken aus, darunter auch Informationen darüber, wie der Garbage Collector ausgelöst wurde, Zeitangaben für die Mark- und Sweep-Phasen sowie eine detaillierte Aufschlüsselung der Speichernutzung nach Bytes und Adressraumblöcken.

© 2026 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.