Gestion de la mémoire JavaScript





Bonne journée, mes amis!



Dans la grande majorité des cas, en tant que développeurs JavaScript, nous n'avons pas à nous soucier de travailler avec la mémoire. Le moteur le fait pour nous.



Cependant, un jour, vous rencontrerez un problème appelé "fuite de mémoire", qui ne peut être résolu qu'en sachant comment la mémoire est allouée en JavaScript.



Dans cet article, j'expliquerai le fonctionnement de l'allocation de mémoire et du garbage collection, et comment éviter certains des problèmes courants associés aux fuites de mémoire.



Cycle de vie de la mémoire



Lorsque vous créez une variable ou une fonction, le moteur JavaScript lui alloue de la mémoire et la libère lorsqu'elle n'est plus nécessaire.



L'allocation de mémoire est le processus de réservation d'un espace spécifique dans la mémoire, et la libération de mémoire libère cet espace afin qu'il puisse être utilisé à d'autres fins.



Chaque fois qu'une variable ou une fonction est créée, la mémoire passe par les étapes suivantes:







  • Allocation de mémoire - le moteur alloue automatiquement de la mémoire pour l'objet créé
  • Utilisation de la mémoire - la lecture et l'écriture de données dans la mémoire ne sont rien de plus que l'écriture et la lecture de données à partir d'une variable
  • Libération de la mémoire - cette étape est également effectuée automatiquement par le moteur. Une fois la mémoire libérée, elle peut être utilisée à d'autres fins.


Heap and stack



La question suivante est: que signifie la mémoire? Où les données sont-elles réellement stockées?



Le moteur a deux de ces emplacements: le tas et la pile. Heap et Stack sont des structures de données utilisées par le moteur à des fins différentes.



Pile: allocation de mémoire statique







Toutes les données de l'exemple sont stockées sur la pile car elles sont primitives.



La pile est une structure de données utilisée pour stocker des données statiques. Les données statiques sont des données dont la taille est connue du moteur au stade de la compilation du code. En JavaScript, ces données sont des primitives (chaînes, nombres, booléens, non définis et null) et des références qui pointent vers des objets et des fonctions.



Puisque le moteur sait que la taille des données ne changera pas, il alloue une taille de mémoire fixe pour chaque valeur. Le processus d'allocation de mémoire avant d'exécuter votre code est appelé allocation de mémoire statique. Étant donné que le moteur alloue une taille de mémoire fixe, il existe certaines limites à cette taille, qui dépendent fortement du navigateur.



Heap: allocation de mémoire dynamique



Le tas sert à stocker des objets et des fonctions. Contrairement à la pile, le moteur n'alloue pas une taille de mémoire fixe pour les objets. La mémoire est allouée selon les besoins. Cette allocation de mémoire est appelée dynamique. Voici un petit tableau de comparaison:



Empiler Tas
Valeurs primitives et références Objets et fonctions
La taille est connue au moment de la compilation La taille est connue à l'exécution
Mémoire fixe allouée La taille de la mémoire pour chaque objet n'est pas limitée


Exemples de



Regardons quelques exemples.



  const person = {
    name: "John",
    age: 24,
  };


Le moteur alloue de la mémoire pour cet objet sur le tas. Cependant, les valeurs de propriété sont stockées sur la pile.



  const hobbies = ["hiking", "reading"];


Les tableaux sont des objets, ils sont donc stockés sur le tas



  let name = "John";
  const age = 24;

  name = "John Doe";
  const firstName = name.slice(0, 4);


Les primitifs sont immuables. Cela signifie qu'au lieu de changer la valeur d'origine, JavaScript en crée une nouvelle.



Liens



Toutes les variables sont stockées sur la pile. Dans le cas de valeurs non primitives, la pile stocke des références à un objet sur le tas. La mémoire sur le tas est désordonnée. C'est pourquoi nous avons besoin de liens sur la pile. Vous pouvez considérer les liens comme des adresses et les objets comme des maisons à une adresse spécifique.







Dans l'image ci-dessus, nous pouvons voir comment les différentes valeurs sont stockées. Notez que personne et newPerson pointent vers le même objet



Exemples de



  const person = {
    name: "John",
    age: 24,
  };


Cela crée un nouvel objet sur le tas et une référence à celui-ci sur la pile



Collecte des ordures



Dès que le moteur constate qu'une variable ou une fonction n'est plus utilisée, il libère la mémoire qu'elle occupe.



En fait, le problème de la libération de la mémoire inutilisée est insoluble: il n'y a pas d'algorithme parfait pour le résoudre.



Dans cet article, nous examinerons deux algorithmes qui offrent les meilleures solutions à ce jour: le comptage des références ramasse-miettes et le marquage et le balayage.



Garbage collection via le comptage de références



Tout est simple ici - des objets sur lesquels aucun point de référence n'est supprimé de la mémoire. Regardons un exemple. Les lignes représentent des liens.







Notez que seul l'objet "hobbies" reste sur le tas, puisque seul cet objet est référencé sur la pile.



Liens cycliques



Le problème avec cette méthode de garbage collection est l'incapacité de définir des références circulaires. Il s'agit d'une situation dans laquelle deux objets ou plus se pointent l'un vers l'autre mais n'ont pas de xréfs. Ceux. ces objets ne sont pas accessibles de l'extérieur.



  const son = {
    name: "John",
  };

  const dad = {
    name: "Johnson",
  };

  son.dad = dad;
  dad.son = son;

  son = null;
  dad = null;






Puisque les objets "fils" et "papa" se réfèrent l'un à l'autre, l'algorithme de comptage de référence ne peut pas libérer de la mémoire. Cependant, ces objets ne sont plus disponibles pour le code externe.



Algorithme de marquage et de nettoyage



Cet algorithme résout le problème des références circulaires. Au lieu de compter les références qui pointent vers un objet, il détermine l'accessibilité de l'objet à partir de l'objet racine. L'objet racine est l'objet "fenêtre" dans le navigateur ou l'objet "global" dans Node.js.







L'algorithme marque les objets comme inaccessibles et les supprime. Ainsi, les références circulaires ne sont plus un problème. Dans l'exemple ci-dessus, les objets "papa" et "fils" sont inaccessibles depuis l'objet racine. Ils seront marqués comme corbeille et supprimés. L'algorithme en question est implémenté dans tous les navigateurs modernes depuis 2012. Les améliorations apportées depuis lors concernent la mise en œuvre et l'amélioration des performances, mais pas l'idée de base de l'algorithme.



Des compromis



Le garbage collection automatique nous permet de nous concentrer sur la création d'applications et de ne pas perdre de temps sur la gestion de la mémoire. Cependant, tout a un prix.



Utilisation de la mémoire



Étant donné que les algorithmes mettent du temps à déterminer que la mémoire n'est plus utilisée, les applications JavaScript ont tendance à utiliser plus de mémoire qu'elles n'en ont réellement besoin.



Même si les objets sont marqués comme des déchets, le collecteur doit décider quand les collecter afin de ne pas bloquer le déroulement du programme. Si vous voulez que votre application soit aussi efficace que possible en termes d'utilisation de la mémoire, il vaut mieux utiliser un langage de programmation de niveau inférieur. Mais gardez à l'esprit que ces langues ont leurs propres compromis.



Performance



Les algorithmes de récupération de place s'exécutent périodiquement pour nettoyer les objets inutilisés. Le problème est que nous, en tant que développeurs, ne savons pas exactement quand cela se produira. De grandes quantités de garbage collection ou de garbage collection fréquente peuvent affecter les performances car elles nécessitent une certaine puissance de traitement. Cependant, cela se passe généralement inaperçu par l'utilisateur et le développeur.



Fuites de mémoire



Jetons un coup d'œil aux problèmes de fuite de mémoire les plus courants.



Variables globales



Si vous déclarez une variable sans utiliser l'un des mots-clés (var, let ou const), la variable devient une propriété de l'objet global.



  users = getUsers();


L'exécution de votre code en mode strict évite cela.



Parfois, nous déclarons volontairement des variables globales. Dans ce cas, afin de libérer la mémoire occupée par une telle variable, vous devez lui attribuer la valeur "null":



  window.users = null;


Minuteries et rappels oubliés



Si vous oubliez les minuteries et les rappels, l'utilisation de la mémoire de votre application peut augmenter considérablement. Soyez prudent, en particulier lors de la création d'applications à page unique (SPA) dans lesquelles des gestionnaires d'événements et des rappels sont ajoutés de manière dynamique.



Minuteries oubliées



  const object = {};
  const intervalId = setInterval(function () {
    // ,   ,      ,
    //   ,     
    doSomething(object);
  }, 2000);


Le code ci-dessus exécute la fonction toutes les 2 secondes. Si vous n'avez plus besoin de la minuterie, vous devez l'annuler en:



  clearInterval(intervalId);


Ceci est particulièrement important pour SPA. Même si vous accédez à une autre page où le minuteur n'est pas utilisé, il fonctionnera en arrière-plan.



Rappels oubliés



Supposons que vous enregistrez un gestionnaire pour un clic de bouton que vous supprimez ultérieurement. En fait, ce n'est plus un problème, mais il est toujours recommandé de supprimer les gestionnaires qui ne sont plus nécessaires:



  const element = document.getElementById("button");
  const onClick = () => alert("hi");

  element.addEventListener("click", onClick);

  element.removeEventListener("click", onClick);
  element.parentNode.removeChild(element);


Liens en dehors du DOM



Cette fuite mémoire est similaire aux précédentes, elle se produit lors du stockage d'éléments DOM en JavaScript:



  const elements = [];
  const element = document.getElementById("button");
  elements.push(element);

  function removeAllElements() {
    elements.forEach((item) => {
      document.body.removeChild(document.getElementById(item.id));
    });
  }


Si vous supprimez l'un de ces éléments, vous devez également le supprimer du tableau. Sinon, ces éléments ne peuvent pas être supprimés par le garbage collector:



  const elements = [];
  const element = document.getElementById("button");
  elements.push(element);

  function removeAllElements() {
    elements.forEach((item, index) => {
      document.body.removeChild(document.getElementById(item.id));
      elements.splice(index, 1);
    });
  }


J'espère que vous avez trouvé quelque chose d'intéressant pour vous-même. Merci de votre attention.



All Articles