Sur cette page

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 ne devez la lire que si vous vous intéressez aux caractéristiques exactes de la gestion de la mémoire JavaScript dans QML. En particulier, elle peut être utile si vous essayez d'optimiser votre application pour obtenir des performances maximales.

Remarque : en compilant votre code QML en C++ à l'aide du compilateurQt Quick , 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++ familiers 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 reviendra à l'interprétation ou à la compilation JIT et utilisera des objets JavaScript stockés dans 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 sous forme d'unités de plusieurs pages. Les objets, les chaînes de caractères et les autres valeurs gérées créés en JavaScript sont ensuite placés dans cet espace d'adressage, en utilisant le propre schéma d'allocation du moteur JavaScript. Le moteur JavaScript n'utilise pas les fonctions malloc() et free() de la bibliothèque C, ni les implémentations par défaut des fonctions new et delete de C++ pour allouer de la mémoire aux objets JavaScript.

Les demandes d'espace d'adressage sont généralement effectuées avec mmap() sur les systèmes de type Unix et avec VirtualAlloc() sur Windows. Il existe plusieurs implémentations de ces primitives spécifiques à chaque plateforme. L'espace d'adressage réservé de cette manière n'est pas immédiatement enregistré dans la mémoire physique. Le système d'exploitation remarque plutôt qu'une page de mémoire est effectivement accédée et ne l'enregistre qu'à ce moment-là. Par conséquent, l'espace d'adressage est pratiquement libre et le fait d'en avoir beaucoup donne au gestionnaire de mémoire JavaScript l'effet de levier dont il a besoin pour placer les objets de manière efficace sur le tas JavaScript. En outre, il existe des techniques spécifiques aux plates-formes pour indiquer au système d'exploitation qu'une partie de l'espace d'adressage, bien que toujours réservée, ne doit pas être mappée dans la mémoire physique pour le moment. Le système d'exploitation peut alors désengager la mémoire si nécessaire et l'utiliser pour d'autres tâches. Il est important de noter que la plupart des systèmes d'exploitation ne garantissent pas une action immédiate à la suite d'une telle demande de désaffectation. Ils ne désengagent la mémoire que lorsqu'elle est réellement nécessaire pour autre chose. Sur les systèmes de type Unix, nous utilisons généralement madvise() à cette fin. Windows dispose de drapeaux spécifiques à VirtualFree() pour faire l'équivalent.

Remarque : certains outils de profilage de la mémoire ne comprennent pas ce mécanisme et surévaluent l'utilisation de la mémoire JavaScript.

Toutes les valeurs stockées dans le tas JavaScript sont soumises au ramassage des ordures. Aucune des valeurs n'est immédiatement "supprimée" lorsqu'elle sort du champ d'application ou est autrement "abandonnée". Seul le ramasse-miettes peut retirer des valeurs du tas JavaScript et restituer de la mémoire (voir le ramasse-miettes ci-dessous pour savoir comment cela fonctionne).

Types basés sur QObject

QObject-Les types basés sur QObject, et en particulier tout ce que vous pouvez exprimer en tant qu'élément QML, sont alloués sur le tas C++. Seule une petite enveloppe autour du pointeur est placée sur le tas JavaScript lorsqu'on accède à QObject à partir de JavaScript. Cette enveloppe peut toutefois posséder le site QObject vers lequel elle pointe. Voir QJSEngine::ObjectOwnership. Si l'enveloppe possède l'objet, celui-ci sera supprimé lorsque l'enveloppe sera ramassée. Vous pouvez également déclencher manuellement la suppression en appelant la méthode destroy(). 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 d' objets déclarées en QML sont stockées sur le tas JavaScript. Elles vivent aussi longtemps que l'objet auquel elles appartiennent vit. Ensuite, elles sont 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 de fonction, les tableaux, les expressions régulières, les objets de date et bien d'autres choses encore. QML possède 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 attribue un espace de stockage sur le tas JavaScript.

Les chaînes 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 enveloppes QObject, les objets du tas pour les chaînes ne sont que de minces enveloppes autour d'un pointeur sur les données de la chaîne.

Lors de l'allocation de mémoire pour un objet, la taille de l'objet est d'abord arrondie à un alignement de 32 octets. Chaque morceau d'espace d'adressage de 32 octets est appelé "slot". Pour les objets dont la taille est inférieure à un seuil "énorme", 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 liées de morceaux de tas précédemment libérés, appelées "bacs". Chaque emplacement contient des morceaux de tas avec une taille fixe par emplacement dans des slots. Si le bac correspondant à la bonne taille n'est pas vide, il choisit 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 par l'intermédiaire d'un allocateur d'espace de stockage (bumper allocator). Un pointeur de pare-chocs 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 augmenté en conséquence et l'objet est placé dans l'espace inutilisé.
  • Une corbeille distincte est conservée pour les morceaux de tas précédemment libérés, de tailles variables supérieures aux tailles spécifiques mentionnées ci-dessus. Le gestionnaire de mémoire parcourt cette liste et tente de trouver un morceau qu'il peut diviser pour accueillir le nouvel objet.
  • Le gestionnaire de mémoire recherche les listes de bacs de taille spécifique plus grande que l'objet à allouer et tente de diviser l'un d'entre eux.
  • 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 volumineux sont gérés par leur propre allocateur. Pour chacun d'entre eux, une ou plusieurs pages de mémoire distinctes sont obtenues auprès du système d'exploitation et gérées séparément.

En outre, chaque nouveau morceau d'espace d'adressage que le gestionnaire de mémoire obtient du système d'exploitation reçoit un en-tête qui contient un certain nombre de drapeaux pour chaque emplacement :

  • objet: Le premier emplacement occupé par un objet est marqué par ce bit.
  • extends: Tout autre emplacement occupé par un objet est marqué 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 le stockage des métadonnées relatives aux membres d'un objet, le moteur JavaScript attribue une "classe interne" à chaque objet. D'autres moteurs JavaScript appellent cela "classe cachée" ou "forme". Les classes internes sont dédupliquées et conservées dans un arbre. 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 disposition d'objet s'est déjà produite auparavant. Si c'est le cas, nous pouvons utiliser immédiatement la classe interne résultante. Dans le cas contraire, nous devons en créer une nouvelle.

Les classes internes sont stockées dans leur propre section du tas JavaScript, qui fonctionne par ailleurs de la même manière que l'allocation générale d'objets décrite ci-dessus. En effet, les classes internes doivent être maintenues en vie pendant que les objets qui les utilisent sont collectés. Les classes internes sont alors collectées dans une passe séparée.

Les attributs de propriété stockés dans les classes internes ne sont pas conservés dans le tas JavaScript, mais plutôt gérés à l'aide de new et delete.

Le ramasse-miettes

Le ramasse-miettes utilisé dans le moteur JavaScript est un modèle non mobile, Mark and Sweep. Depuis Qt 6.8, il fonctionne par défaut de manière incrémentale (à moins que QV4_GC_TIMELIMIT ne soit fixé à 0). Dans la phase de marquage, nous parcourons tous les endroits connus où des références vivantes à des objets peuvent être trouvées. En particulier :

  • les 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 QJSValue et les classes similaires conservent les références aux objets JavaScript.

Pour tout objet trouvé dans ces endroits, les bits de marquage sont définis récursivement pour tout ce qu'il référence.

Dans la phase de balayage, le ramasse-miettes parcourt ensuite l'ensemble du tas et libère tous les objets qui n'ont pas été marqués auparavant. La mémoire libérée qui en résulte est triée dans les bacs qui seront utilisés pour d'autres allocations. Si un morceau de l'espace d'adressage est complètement vide, il est désengagé, mais l'espace d'adressage est conservé (voir les principes de base ci-dessus). Si l'utilisation de la mémoire augmente à nouveau, le 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 l'objet sur le tas JavaScript, mais qui n'est pas directement allouée sur le tas JavaScript, comme les chaînes de caractères et les données internes des membres de la classe. Un seuil dynamique est maintenu pour ces données. 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 la mémoire interne sur le tas JavaScript n'est envisagée qu'après qu'un espace d'adressage a été réservé.
  • L'espace d'adressage supplémentaire réservé depuis la dernière exécution du ramasse-miettes. Si la quantité d'espace d'adressage est plus de deux fois supérieure à la quantité de mémoire utilisée après la dernière exécution de l'éboueur, nous relançons l'éboueur.

Analyse de l'utilisation de la mémoire

Pour observer l'évolution de l'espace d'adressage et du nombre d'objets qui y sont alloués, il est préférable d'utiliser un outil spécialisé. L'outil QML Profiler fournit une visualisation qui est utile dans ce cas. Les outils plus génériques ne peuvent pas voir ce que le gestionnaire de mémoire JavaScript fait dans l'espace d'adressage qu'il réserve et peuvent même ne pas remarquer qu'une partie de l'espace d'adressage n'est pas engagée dans la mémoire physique.

Les sites logging categories qt.qml.gc.statistics et qt.qml.gc.allocatorStats constituent un autre moyen de déboguer l'utilisation de la mémoire. Si vous activez le niveau Debug pour qt.qml.gc.statistics, le garbage collector affichera certaines informations à chaque fois qu'il s'exécutera :

  • combien d'espace d'adressage total est réservé
  • la quantité de mémoire utilisée avant et après le ramassage des ordures
  • Combien d'objets de différentes tailles ont été alloués jusqu'à présent.

Le niveau Debug de qt.qml.gc.allocatorStats imprime des statistiques plus détaillées qui comprennent également la façon dont le ramasse-miettes a été déclenché, la durée des phases de marquage et de balayage et une répartition détaillée de l'utilisation de la mémoire par octets et par morceaux de l'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.