Gestion de la mémoire dans le moteur JavaScript
Introduction
Ce document décrit la gestion dynamique de la mémoire du moteur JavaScript dans QML. Il s’agit d’une description assez technique et approfondie. Vous n’avez besoin de le lire que si vous vous intéressez aux caractéristiques précises de la gestion de la mémoire JavaScript dans QML. Cela peut notamment s’avérer utile si vous cherchez à optimiser votre application pour en tirer les meilleures performances possibles.
Remarque : en compilant votre code QML en C++ à l'aide de la Qt Quick Compiler vous pouvez éviter une grande partie de l’utilisation du tas JavaScript. Le code C++ généré utilise la pile et le tas C++ habituels pour stocker les objets et les valeurs. L’environnement hôte JavaScript, cependant, utilise toujours une partie de la mémoire gérée par JavaScript, que vous l’utilisiez ou non. Si vous utilisez des fonctionnalités qui ne peuvent pas être compilées en C++, le moteur aura toutefois recours à l’interprétation ou à la compilation JIT et utilisera des objets JavaScript stockés sur le tas JavaScript.
Principes de base
Le moteur JavaScript de QML dispose d’un gestionnaire de mémoire dédié qui demande au système d’exploitation un espace d’adressage par tranches de plusieurs pages. Les objets, chaînes de caractères et autres valeurs gérées créés en JavaScript sont ensuite placés dans cet espace d’adressage, selon le schéma d’allocation propre au moteur JavaScript. Le moteur JavaScript n’utilise ni les fonctions malloc() et free() de la bibliothèque C, ni les implémentations par défaut des opérateurs new et delete de C++ pour allouer de la mémoire aux objets JavaScript.
Les demandes d’espace d’adressage s’effectuent généralement via `mmap()` sur les systèmes de type Unix et via `VirtualAlloc()` sous Windows. Il existe plusieurs implémentations spécifiques à chaque plateforme de ces primitives. L’espace d’adressage ainsi réservé n’est pas immédiatement alloué en mémoire physique. Au contraire, le système d’exploitation détecte lorsqu’une page de mémoire est réellement accédée et ne l’alloue qu’à ce moment-là. Par conséquent, l’espace d’adressage est pratiquement gratuit et en disposer en grande quantité donne au gestionnaire de mémoire JavaScript la marge de manœuvre nécessaire pour placer les objets de manière efficace sur le tas JavaScript. De plus, il existe des techniques spécifiques à chaque plateforme pour indiquer au système d’exploitation qu’un bloc d’espace d’adressage, bien que toujours réservé, n’a pas besoin d’être mappé en mémoire physique pour le moment. Le système d’exploitation peut alors libérer la mémoire selon les besoins et l’utiliser pour d’autres tâches. Il est essentiel de noter que la plupart des systèmes d’exploitation ne garantissent pas une réponse immédiate à une telle demande de libération. Ils ne libèrent la mémoire que lorsqu’elle est réellement nécessaire à autre chose. Sur les systèmes de type Unix, on utilise généralement la fonction madvise() à cette fin. Sous Windows, la fonction VirtualFree() dispose de drapeaux spécifiques permettant d’obtenir un résultat équivalent.
Remarque : certains outils de profilage de la mémoire ne comprennent pas ce mécanisme et surestiment l’utilisation de la mémoire par JavaScript.
Toutes les valeurs stockées dans le tas JavaScript sont soumises au ramasse-miettes. Aucune de ces valeurs n’est immédiatement « supprimée » lorsqu’elle sort de sa portée ou est « abandonnée » d’une autre manière. Seul le ramasse-miettes peut retirer des valeurs du tas JavaScript et libérer de la mémoire (voir la section « Ramasse-miettes » ci-dessous pour comprendre son fonctionnement).
Types basés sur QObject
QObjectLes types basés sur QObject, et en particulier tout ce que l’on peut définir comme un élément QML, sont alloués sur le tas C++. Seul un petit wrapper autour du pointeur est placé sur le tas JavaScript lorsqu’on accède à un QObject depuis JavaScript. Un tel wrapper peut toutefois posséder l’ QObject e vers laquelle il pointe. Voir QJSEngine::ObjectOwnership. Si le wrapper détient l’objet, celui-ci sera supprimé lorsque le wrapper sera récupéré par le ramasse-miettes. Vous pouvez également déclencher manuellement la suppression en appelant la méthode `destroy()` sur ce wrapper. `destroy()` appelle en interne ` QObject::deleteLater()`. Elle ne supprimera donc pas immédiatement l’objet, mais attendra la prochaine itération de la boucle d’événements.
Les propriétés des objets déclarées en QML sont stockées dans le tas JavaScript. Elles persistent tant que l’objet auquel elles appartiennent existe. Elles sont ensuite supprimées lors de la prochaine exécution du ramasse-miettes.
Allocation d’objets
En JavaScript, tout type structuré est un objet. Cela inclut les objets fonction, les tableaux, les expressions régulières, les objets date et bien d’autres encore. QML dispose d’un certain nombre de types d’objets internes, tels que le wrapper « QObject » mentionné ci-dessus. Chaque fois qu’un objet est créé, le gestionnaire de mémoire lui alloue de l’espace sur le tas JavaScript.
Les chaînes de caractères JavaScript sont également des valeurs gérées, mais leurs données de chaîne ne sont pas allouées sur le tas JavaScript. À l’instar des wrappers « QObject », les objets du tas correspondant aux chaînes de caractères ne sont que de simples wrappers légers autour d’un pointeur vers les données de la chaîne.
Lors de l’allocation de mémoire pour un objet, la taille de celui-ci est d’abord arrondie à l’alignement de 32 octets. Chaque bloc de 32 octets de l’espace d’adressage est appelé un « slot ». Pour les objets dont la taille est inférieure à un seuil dit « huge size », le gestionnaire de mémoire effectue une série de tentatives pour placer l’objet en mémoire :
- Le gestionnaire de mémoire conserve des listes chaînées des blocs de mémoire du tas précédemment libérés, appelés « bins ». Chaque bin contient des blocs de mémoire du tas d’une taille fixe par bin, répartis dans des slots. Si le bin correspondant à la bonne taille n’est pas vide, il sélectionne la première entrée et y place l’objet.
- La mémoire qui n’a pas encore été utilisée est gérée via un allocateur « bumper ». Un pointeur « bumper » pointe vers l’octet situé au-delà de l’espace d’adressage occupé. S’il reste suffisamment d’espace d’adressage inutilisé, le « bumper » est agrandi en conséquence, et l’objet est placé dans cet espace inutilisé.
- Un compartiment distinct est réservé aux parties du tas précédemment libérées, de tailles variables mais supérieures aux tailles spécifiques mentionnées ci-dessus. Le gestionnaire de mémoire parcourt cette liste et tente de trouver une partie qu’il peut diviser pour accueillir le nouvel objet.
- Le gestionnaire de mémoire parcourt les listes de compartiments de taille spécifique, supérieurs à celle de l’objet à allouer, et tente d’en diviser un.
- Enfin, si aucune des solutions ci-dessus ne fonctionne, le gestionnaire de mémoire réserve davantage d’espace d’adressage et alloue l’objet à l’aide de l’allocateur « bumper ».
Les objets de très grande taille sont gérés par leur propre allocateur. Pour chacun d’entre eux, une ou plusieurs pages mémoire distinctes sont obtenues auprès du système d’exploitation et gérées séparément.
De plus, chaque nouveau bloc d’espace d’adressage que le gestionnaire de mémoire obtient du système d’exploitation est doté d’un en-tête contenant un certain nombre d’indicateurs pour chaque emplacement :
- objet: le premier emplacement occupé par un objet est marqué par ce bit.
- extends: tous les emplacements supplémentaires occupés par un objet sont marqués par ce bit.
- mark: lorsque le ramasse-miettes s’exécute, il active ce bit si l’objet est toujours en cours d’utilisation.
Classes internes
Afin de minimiser l'espace de stockage nécessaire aux métadonnées concernant les membres d'un objet, le moteur JavaScript attribue une « classe interne » à chaque objet. D'autres moteurs JavaScript appellent cela une « classe cachée » ou une « forme ». Les classes internes sont dédupliquées et conservées dans une arborescence. Si une propriété est ajoutée à un objet, les enfants de la classe interne actuelle sont vérifiés pour voir si la même structure d’objet s’est déjà produite auparavant. Si c’est le cas, on peut utiliser immédiatement la classe interne résultante. Sinon, il faut en créer une nouvelle.
Les classes internes sont stockées dans une section distincte du tas JavaScript, qui fonctionne par ailleurs de la même manière que le processus général d’allocation d’objets décrit ci-dessus. En effet, les classes internes doivent rester actives pendant que les objets qui les utilisent sont collectés. Les classes internes sont ensuite collectées lors d’un passage distinct.
Les attributs de propriété proprement dits stockés dans les classes internes ne sont toutefois pas conservés dans le tas JavaScript, mais gérés à l’aide des opérateurs `new` et `delete`.
Collecte des déchets
Le ramasse-miettes utilisé dans le moteur JavaScript est de type « Mark and Sweep » (marquage et balayage), sans déplacement. Depuis Qt 6.8, il s’exécute de manière incrémentielle par défaut (à moins que QV4_GC_TIMELIMIT ne soit défini sur 0). Au cours de la phase de marquage, nous parcourons tous les emplacements connus où se trouvent des références actives à des objets. En particulier :
- les variables globales JavaScript
- Les parties non supprimables des unités de compilation QML et JavaScript
- La pile JavaScript
- Le stockage des valeurs persistantes. C'est là que les classes ` QJSValue ` et autres classes similaires conservent des références aux objets JavaScript.
Pour tout objet trouvé à ces emplacements, les bits de marquage sont définis de manière récursive pour tout ce à quoi il fait référence.
Au cours de la phase de balayage, le ramasse-miettes parcourt alors l’ensemble du tas et libère tous les objets qui n’avaient pas été marqués auparavant. La mémoire ainsi libérée est triée dans des compartiments en vue d’allocations ultérieures. Si un bloc d’espace d’adressage est complètement vide, il est délibéré, mais l’espace d’adressage est conservé (voir les principes de base ci-dessus). Si l’utilisation de la mémoire augmente à nouveau, ce même espace d’adressage est réutilisé.
Le ramasse-miettes est déclenché soit manuellement en appelant la fonction ` gc()`, soit par une heuristique qui prend en compte les aspects suivants :
- La quantité de mémoire gérée par les objets sur le tas JavaScript, mais non directement allouée sur ce tas, comme les chaînes de caractères et les données internes des membres de classe. Un seuil dynamique est maintenu pour celles-ci. S’il est dépassé, le ramasse-miettes s’exécute et le seuil est augmenté. Si la quantité de mémoire externe gérée tombe bien en dessous du seuil, celui-ci est diminué.
- L’espace d’adressage total réservé. L’allocation de mémoire interne sur le tas JavaScript n’est prise en compte qu’après qu’au moins une partie de l’espace d’adressage a été réservée.
- La réservation d’espace d’adressage supplémentaire depuis la dernière exécution du ramasse-miettes. Si l’espace d’adressage réservé est supérieur au double de la quantité de mémoire utilisée après la dernière exécution du ramasse-miettes, nous relançons le ramasse-miettes.
Analyse de l’utilisation de la mémoire
Afin d’observer l’évolution tant de l’espace d’adressage que du nombre d’objets qui y sont alloués, il est préférable d’utiliser un outil spécialisé. Le QML Profiler fournit une visualisation utile à cet effet. Les outils plus génériques ne permettent pas de voir ce que fait le gestionnaire de mémoire JavaScript au sein de l’espace d’adressage qu’il réserve et peuvent même ne pas remarquer qu’une partie de cet espace n’est pas allouée en mémoire physique.
Les logging categories qt.qml.gc.statistics et qt.qml.gc.allocatorStats constituent une autre méthode pour déboguer l’utilisation de la mémoire. Si vous activez le niveau de débogage pour qt.qml.gc.statistics, le ramasse-miettes affichera certaines informations à chaque exécution :
- La quantité totale d’espace d’adressage réservée
- La quantité de mémoire utilisée avant et après le ramasse-miettes
- le nombre d’objets de différentes tailles alloués jusqu’à présent
Le niveau « Debug » pour qt.qml.gc.allocatorStats affiche des statistiques plus détaillées, qui incluent également la manière dont le ramasse-miettes a été déclenché, les durées des phases de marquage et de balayage, ainsi qu’une ventilation détaillée de l’utilisation de la mémoire en octets et en blocs d’espace d’adressage.
© 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.