Cette section décrit les termes couramment utilisés dans l'analyse de la mémoire et s'applique à divers outils de profilage de la mémoire pour différentes langues.
Les termes et notions décrits ici font référence au profileur de tas des outils pour les développeurs Chrome. Si vous avez déjà travaillé avec Java, .NET ou un autre profileur de mémoire, cet article peut vous rafraîchir la mémoire.
Tailles des objets
Considérez la mémoire comme un graphique avec des types primitifs (comme les nombres et les chaînes) et des objets (tableaux associatifs). Il peut être représenté visuellement sous la forme d'un graphique avec un certain nombre de points interconnectés, comme suit:
Un objet peut stocker de la mémoire de deux manières:
- Directement par l'objet lui-même.
- Implicitement en conservant des références à d'autres objets, ce qui empêche ces objets d'être automatiquement supprimés par un garbage collector (GC pour "garbage collector").
Lorsque vous utilisez le Profileur de tas dans DevTools (un outil permettant d'examiner les problèmes de mémoire détectés dans le panneau Mémoire), vous vous trouverez probablement à examiner plusieurs colonnes d'informations différentes. Deux valeurs se démarquent : Taille superficielle et Taille conservée. Mais que représentent-elles ?
Taille peu profonde
Il s'agit de la taille de la mémoire détenue par l'objet lui-même.
Les objets JavaScript typiques disposent d'une mémoire réservée pour leur description et pour stocker des valeurs immédiates. En règle générale, seuls les tableaux et les chaînes peuvent avoir une taille peu profonde importante. Toutefois, les chaînes et les tableaux externes ont souvent leur stockage principal dans la mémoire du moteur de rendu, n'exposant qu'un petit objet wrapper sur le tas de mémoire JavaScript.
La mémoire du moteur de rendu correspond à l'ensemble de la mémoire du processus dans lequel une page inspectée est affichée: mémoire native + mémoire de tas JS de la page + mémoire de tas JS de tous les nœuds de calcul dédiés démarrés par la page. Néanmoins, même un petit objet peut contenir indirectement une grande quantité de mémoire en empêchant d'autres objets d'être supprimés par le processus de récupération automatique des déchets.
Taille conservée
Il s'agit de la taille de mémoire libérée une fois que l'objet lui-même a été supprimé, ainsi que ses objets dépendants qui sont devenus inaccessibles à partir des racines GC.
Les racines GC sont constituées de handles créés (locaux ou globaux) lors de la création d'une référence à partir du code natif vers un objet JavaScript en dehors de V8. Tous ces handles se trouvent dans un instantané de tas sous GC roots > Handle scope (Racines GC > Champ d'application des handles) et GC roots > Global handles (Racines GC > Handles globaux). Décrire les poignées dans cette documentation sans entrer dans les détails de l'implémentation du navigateur peut prêter à confusion. Vous n'avez pas à vous soucier des racines GC ni des poignées.
Il existe de nombreuses racines GC internes, dont la plupart ne sont pas intéressantes pour les utilisateurs. Du point de vue des applications, il existe les types de racines suivants:
- Objet global de la fenêtre (dans chaque iFrame). Un champ de distance est présent dans les instantanés de tas, qui correspond au nombre de références de propriété sur le chemin de rétention le plus court à partir de la fenêtre.
- Arborescence DOM du document composée de tous les nœuds DOM natifs accessibles en parcourant le document. Il est possible qu'ils ne disposent pas tous de wrappers JS, mais s'ils en ont, ils resteront actifs tant que le document l'est.
- Parfois, des objets peuvent être conservés par le contexte du débogueur et la console DevTools (par exemple, après l'évaluation de la console). Créez des instantanés de tas de mémoire avec une console claire et sans points d'arrêt actifs dans le débogueur.
Le graphique de mémoire commence par une racine, qui peut être l'objet window
du navigateur ou l'objet Global
d'un module Node.js. Vous ne contrôlez pas la façon dont cet objet racine est collecté.
Tout ce qui n'est pas accessible depuis la racine est collecté.
Arbre de conservation des objets
La pile est un réseau d'objets interconnectés. Dans le monde mathématique, cette structure est appelée graphe ou graphe de mémoire. Un graphique est construit à partir de nœuds connectés par des arêtes, auxquels sont attribués des libellés.
- Les nœuds (ou objets) sont libellés à l'aide du nom de la fonction de constructeur utilisée pour les créer.
- Les arêtes sont libellées à l'aide des noms des propriétés.
Découvrez comment enregistrer un profil à l'aide de l'outil Heap Profiler. Parmi les éléments frappants que nous pouvons voir dans l'enregistrement du profileur de tas suivant, citons la distance: la distance par rapport à la racine du GC. Si presque tous les objets du même type sont à la même distance et que quelques-uns sont à une distance plus éloignée, cela vaut la peine d'examiner ce point.
Dominators
Les objets dominateur sont constitués d'une structure arborescente, car chaque objet possède exactement un dominateur. Un dominateur d'un objet peut ne pas comporter de références directes à un objet qu'il domine. Autrement dit, l'arbre du dominateur n'est pas un arbre couvrant du graphique.
Dans le schéma suivant :
- Le nœud 1 domine le nœud 2
- Le nœud 2 domine les nœuds 3, 4 et 6
- Le nœud 3 domine le nœud 5
- Le nœud 5 domine le nœud 8
- Le nœud 6 domine le nœud 7
Dans l'exemple suivant, le nœud #3
est le dominateur de #10
, mais #7
existe également dans chaque chemin simple de GC à #10
. Par conséquent, un objet B est un dominateur d'un objet A si B existe dans chaque chemin simple de la racine à l'objet A.
Spécificités de V8
Lorsque vous effectuez un profilage de la mémoire, il est utile de comprendre pourquoi les instantanés de tas ont une certaine apparence. Cette section décrit certains sujets liés à la mémoire qui correspondent spécifiquement à la machine virtuelle JavaScript V8 (VM V8 ou VM).
Représentation d'objet JavaScript
Il existe trois types primitifs:
- Nombres (par exemple, 3,14159..)
- Booléens (vrai ou faux)
- Chaînes (par exemple, 'Werner Heisenberg')
Ils ne peuvent pas faire référence à d'autres valeurs et sont toujours des feuilles ou des nœuds terminaux.
Les nombres peuvent être stockés sous les formes suivantes:
- une valeur entière immédiate de 31 bits appelée petit entier (petit entier) ; ou
- des objets tas, appelés numéros de tas. Les numéros de tas sont utilisés pour stocker des valeurs qui ne correspondent pas au format SMI, telles que les doubles, ou lorsqu'une valeur doit être encadrée, par exemple pour définir des propriétés.
Les chaînes peuvent être stockées dans:
- le tas de mémoire de la VM ; ou
- en externe dans la mémoire du moteur de rendu. Un objet wrapper est créé et utilisé pour accéder au stockage externe où, par exemple, les sources de script et d'autres contenus reçus du Web sont stockés, plutôt que copiés sur le tas de la VM.
La mémoire des nouveaux objets JavaScript est allouée à partir d'un tas de mémoire JavaScript dédié (ou tas de mémoire de VM). Ces objets sont gérés par le garbage collector de V8 et resteront donc actifs tant qu'il existe au moins une référence forte vers eux.
Les objets natifs sont tout ce qui ne se trouve pas dans la pile JavaScript. Contrairement à l'objet tas, l'objet natif n'est pas géré par le garbage collector V8 tout au long de son cycle de vie et n'est accessible qu'à partir de JavaScript à l'aide de son objet wrapper JavaScript.
La chaîne Cons est un objet composé de paires de chaînes stockées puis jointes, et est le résultat d'une concaténation. La jonction des contenus de la chaîne cons ne se produit que si nécessaire. Par exemple, lorsque vous devez créer une sous-chaîne d'une chaîne jointe.
Par exemple, si vous concaténez a et b, vous obtenez une chaîne (a, b) qui représente le résultat de la concaténation. Si vous concatenatez ensuite d avec ce résultat, vous obtenez une autre chaîne cons ((a, b), d).
Tableaux : un tableau est un objet avec des clés numériques. Ils sont largement utilisés dans la VM V8 pour stocker de grandes quantités de données. Les ensembles de paires clé-valeur utilisés comme dictionnaires sont sauvegardés par des tableaux.
Un objet JavaScript typique peut être l'un des deux types de tableaux utilisés pour stocker:
- des propriétés nommées ;
- éléments numériques
Dans les cas où le nombre de propriétés est très faible, elles peuvent être stockées en interne dans l'objet JavaScript lui-même.
Map (Carte) : objet qui décrit le type d'objet et sa mise en page. Par exemple, les cartes sont utilisées pour décrire les hiérarchies d'objets implicites afin d'accéder rapidement aux propriétés.
Groupes d'objets
Chaque groupe d'objets natifs est composé d'objets qui contiennent des références mutuelles les uns aux autres. Prenons l'exemple d'un sous-arbre DOM dans lequel chaque nœud est associé à son parent et aux nœuds enfants et frères suivants, formant ainsi un graphique connecté. Notez que les objets natifs ne sont pas représentés dans la pile JavaScript. C'est pourquoi ils ont une taille nulle. À la place, des objets wrapper sont créés.
Chaque objet wrapper contient une référence à l'objet natif correspondant, pour le rediriger vers les commandes. À son tour, un groupe d'objets contient des objets wrapper. Toutefois, cela ne crée pas de cycle non collectable, car le GC est suffisamment intelligent pour libérer les groupes d'objets dont les wrappers ne sont plus référencés. Toutefois, si vous oubliez de libérer un seul wrapper, l'ensemble du groupe et les wrappers associés seront conservés.