La gestion de la mémoire en JavaScript s'effectue automatiquement et de manière invisible pour nous. On crée des primitives, des objets, des fonctions… Tout cela prend de la mémoire.
Que se passe-t-il lorsque quelque chose n’est plus nécessaire ? Comment le moteur JavaScript le découvre-t-il et le nettoie-t-il ?
Le concept principal de la gestion de la mémoire en JavaScript est l'accessibilité .
En termes simples, les valeurs « accessibles » sont celles qui sont accessibles ou utilisables d’une manière ou d’une autre. Leur conservation en mémoire est garantie.
Il existe un ensemble de valeurs de base intrinsèquement accessibles, qui ne peuvent pas être supprimées pour des raisons évidentes.
Par exemple:
Ces valeurs sont appelées racines .
La fonction en cours d'exécution, ses variables et paramètres locaux.
Autres fonctions sur la chaîne actuelle d'appels imbriqués, leurs variables et paramètres locaux.
Variables globales.
(il y en a aussi d'autres, internes)
Toute autre valeur est considérée comme accessible si elle est accessible depuis une racine par une référence ou par une chaîne de références.
Par exemple, s'il y a un objet dans une variable globale et que cet objet a une propriété faisant référence à un autre objet, cet objet est considéré comme accessible. Et ceux auxquels il fait référence sont également accessibles. Exemples détaillés à suivre.
Il existe un processus en arrière-plan dans le moteur JavaScript appelé garbage collector. Il surveille tous les objets et supprime ceux qui sont devenus inaccessibles.
Voici l'exemple le plus simple :
// l'utilisateur a une référence à l'objet laissez l'utilisateur = { nom : "Jean" } ;
Ici, la flèche représente une référence d'objet. La variable globale "user"
fait référence à l'objet {name: "John"}
(nous l'appellerons John par souci de concision). La propriété "name"
de John stocke une primitive, elle est donc peinte à l'intérieur de l'objet.
Si la valeur de user
est écrasée, la référence est perdue :
utilisateur = nul ;
Maintenant, John devient inaccessible. Il n'y a aucun moyen d'y accéder, aucune référence à celui-ci. Le garbage collector supprimera les données et libérera la mémoire.
Imaginons maintenant que nous copions la référence de user
vers admin
:
// l'utilisateur a une référence à l'objet laissez l'utilisateur = { nom : "Jean" } ; laissez admin = utilisateur ;
Maintenant, si nous faisons la même chose :
utilisateur = nul ;
…Ensuite, l'objet est toujours accessible via la variable globale admin
, il doit donc rester en mémoire. Si nous écrasons également admin
, il peut alors être supprimé.
Maintenant un exemple plus complexe. La famille :
fonction se marier (homme, femme) { femme.mari = homme; homme.femme = femme; retour { père : homme, mère : femme } } laisser la famille = se marier ({ nom : "Jean" }, { prénom : "Anne" });
La fonctionmarie « marry
» deux objets en leur donnant des références l'un à l'autre et renvoie un nouvel objet qui les contient tous les deux.
La structure de mémoire résultante :
Désormais, tous les objets sont accessibles.
Supprimons maintenant deux références :
supprimer famille.père ; supprimer famille.mère.mari ;
Il ne suffit pas de supprimer une seule de ces deux références, car tous les objets resteraient accessibles.
Mais si nous supprimons les deux, alors nous pouvons voir que John n'a plus de référence entrante :
Les références sortantes n'ont pas d'importance. Seuls les objets entrants peuvent rendre un objet accessible. Ainsi, John est désormais inaccessible et sera supprimé de la mémoire avec toutes ses données devenues également inaccessibles.
Après la collecte des déchets :
Il est possible que l'ensemble de l'îlot d'objets interconnectés devienne inaccessible et soit supprimé de la mémoire.
L'objet source est le même que ci-dessus. Alors:
famille = nul ;
L'image en mémoire devient :
Cet exemple montre à quel point le concept d’accessibilité est important.
Il est évident que John et Ann sont toujours liés, tous deux ont de nouvelles références. Mais cela ne suffit pas.
L'ancien objet "family"
a été dissocié de la racine, il n'y a plus de référence à celui-ci, donc l'île entière devient inaccessible et sera supprimée.
L'algorithme de base de collecte des déchets est appelé « mark-and-sweep ».
Les étapes de « garbage collection » suivantes sont régulièrement effectuées :
Le ramasse-miettes prend des racines et les « marque » (se souvient).
Ensuite, il visite et « marque » toutes les références qui en découlent.
Puis il visite les objets marqués et marque leurs références. Tous les objets visités sont mémorisés, afin de ne pas visiter deux fois le même objet dans le futur.
… Et ainsi de suite jusqu'à ce que toutes les références accessibles (depuis les racines) soient visitées.
Tous les objets, à l'exception de ceux marqués, sont supprimés.
Par exemple, la structure de notre objet ressemble à ceci :
Nous pouvons clairement voir une « île inaccessible » sur le côté droit. Voyons maintenant comment le garbage collector « mark-and-sweep » s'en occupe.
La première étape marque les racines :
Ensuite, nous suivons leurs références et marquons les objets référencés :
…Et continuez à suivre d’autres références, dans la mesure du possible :
Désormais, les objets qui n'ont pas pu être visités au cours du processus sont considérés comme inaccessibles et seront supprimés :
Nous pouvons également imaginer le processus comme le déversement d'un énorme seau de peinture depuis les racines, qui traverse toutes les références et marque tous les objets accessibles. Ceux qui ne sont pas marqués sont ensuite supprimés.
C'est le concept du fonctionnement du garbage collection. Les moteurs JavaScript appliquent de nombreuses optimisations pour le rendre plus rapide et n'introduire aucun retard dans l'exécution du code.
Certaines des optimisations :
Collection générationnelle – les objets sont divisés en deux ensembles : les « nouveaux » et les « anciens ». Dans le code typique, de nombreux objets ont une durée de vie courte : ils apparaissent, font leur travail et meurent rapidement. Il est donc logique de suivre les nouveaux objets et d'en effacer la mémoire si tel est le cas. Ceux qui survivent assez longtemps deviennent « vieux » et sont moins souvent examinés.
Collection incrémentielle – s’il y a beaucoup d’objets et que nous essayons de parcourir et de marquer l’ensemble des objets en même temps, cela peut prendre un certain temps et introduire des retards visibles dans l’exécution. Ainsi, le moteur divise l’ensemble des objets existants en plusieurs parties. Et puis effacez ces pièces les unes après les autres. Il existe de nombreuses petites collectes de déchets au lieu d'une collecte totale. Cela nécessite une comptabilité supplémentaire entre eux pour suivre les changements, mais nous obtenons de nombreux petits retards au lieu de gros.
Collecte des temps d'inactivité : le garbage collector tente de s'exécuter uniquement lorsque le processeur est inactif, afin de réduire l'effet possible sur l'exécution.
Il existe d'autres optimisations et versions d'algorithmes de récupération de place. Même si j'aimerais les décrire ici, je dois m'abstenir, car différents moteurs implémentent des réglages et des techniques différents. Et, ce qui est encore plus important, les choses changent à mesure que les moteurs se développent, donc étudier plus profondément « à l'avance », sans réel besoin, n'en vaut probablement pas la peine. À moins bien sûr qu’il ne s’agisse d’une question de pur intérêt, vous trouverez ci-dessous quelques liens pour vous.
Les principales choses à savoir :
La collecte des déchets est effectuée automatiquement. Nous ne pouvons pas le forcer ou l’empêcher.
Les objets sont conservés en mémoire tant qu'ils sont accessibles.
Être référencé n'est pas la même chose qu'être accessible (à partir d'une racine) : un pack d'objets liés entre eux peut devenir inaccessible dans son ensemble, comme nous l'avons vu dans l'exemple ci-dessus.
Les moteurs modernes implémentent des algorithmes avancés de garbage collection.
Un livre général « The Garbage Collection Handbook : The Art of Automatic Memory Management » (R. Jones et al) en couvre certains.
Si vous êtes familier avec la programmation de bas niveau, des informations plus détaillées sur le garbage collector de la V8 se trouvent dans l'article Une visite guidée de la V8 : Garbage Collection.
Le blog V8 publie également de temps en temps des articles sur les changements dans la gestion de la mémoire. Naturellement, pour en savoir plus sur le garbage collection, vous feriez mieux de vous préparer en vous renseignant sur les composants internes du V8 en général et en lisant le blog de Vyacheslav Egorov qui a travaillé comme l'un des ingénieurs du V8. Je dis : « V8 », car il est mieux couvert par des articles sur Internet. Pour d’autres moteurs, de nombreuses approches sont similaires, mais le garbage collection diffère à bien des égards.
Une connaissance approfondie des moteurs est utile lorsque vous avez besoin d'optimisations de bas niveau. Il serait sage de planifier cela comme prochaine étape une fois que vous serez familiarisé avec la langue.