Maison  >  Article  >  interface Web  >  Introduction à la gestion de la mémoire JavaScript + comment gérer 4 fuites de mémoire courantes

Introduction à la gestion de la mémoire JavaScript + comment gérer 4 fuites de mémoire courantes

coldplay.xixi
coldplay.xixiavant
2020-12-09 17:12:492658parcourir

javascriptLa chronique abordera un autre sujet important, la gestion de la mémoire

Introduction à la gestion de la mémoire JavaScript + comment gérer 4 fuites de mémoire courantes

Recommandations d'apprentissage gratuites associées : javascript(Vidéo)

Nous allons Un autre sujet important abordé est la gestion de la mémoire, qui est facilement négligée par les développeurs en raison de la maturité et de la complexité croissantes des langages de programmation utilisés quotidiennement. Nous fournirons également quelques conseils sur la façon de gérer les fuites de mémoire dans JavaScript. Suivre ces conseils dans SessionStack garantira que SessionStack ne provoque pas de fuites de mémoire et n'augmente pas la consommation de mémoire de nos applications Web intégrées.

Si vous souhaitez lire plus d'articles de haute qualité, veuillez cliquer sur le blog GitHub. Des centaines d'articles de haute qualité vous attendent chaque année !

Vue d'ensemble

Les langages de programmation comme C, ont des primitives de gestion de mémoire de bas niveau comme malloc() et free(). Les développeurs utilisent ces primitives pour allouer et libérer explicitement de la mémoire du système d'exploitation.

JavaScript alloue de la mémoire aux objets (objets, chaînes, etc.) lorsqu'ils sont créés, et libère "automatiquement" la mémoire lorsqu'ils ne sont plus utilisés. Ce processus est appelé garbage collection. Cette libération apparemment "automatique" des ressources est une source de confusion car elle donne aux développeurs JavaScript (et autres langages de haut niveau) la fausse impression qu'ils se moquent de la gestion de la mémoire. C'est une grosse erreur.

Même lorsqu'ils travaillent avec des langages de haut niveau, les développeurs doivent comprendre la gestion de la mémoire (ou au moins connaître les bases). Parfois, il y a des problèmes avec la gestion automatique de la mémoire (comme des bugs dans le garbage collector ou des limitations d'implémentation, etc.) et les développeurs doivent comprendre ces problèmes afin qu'ils puissent être traités correctement (ou trouver une solution appropriée qui peut être maintenue avec un minimum de ressources). code d'effort).

Cycle de vie de la mémoire

Quel que soit le langage de programmation utilisé, le cycle de vie de la mémoire est le même :

Introduction à la gestion de la mémoire JavaScript + comment gérer 4 fuites de mémoire courantes

Voici un bref introduction Examinons chaque étape du cycle de vie de la mémoire :

  • Allocation de mémoire — La mémoire est allouée par le système d'exploitation, ce qui permet à votre programme de l'utiliser. Dans un langage de bas niveau (tel que C), il s'agit d'une opération explicitement effectuée que le développeur doit gérer lui-même. Cependant, dans les langages de haut niveau, le système vous attribue automatiquement des éléments intrinsèques.
  • Mémoire utilisée — Il s'agit de la mémoire allouée avant que le programme ne l'utilise réellement, les lectures et les écritures se produisent lorsque les variables allouées sont utilisées dans le code.
  • Libérer la mémoire — Libérez toute la mémoire inutilisée afin qu'elle devienne de la mémoire libre et puisse être réutilisée. Comme les opérations d’allocation de mémoire, cette opération doit également être effectuée explicitement dans les langages de bas niveau.

Qu'est-ce que la mémoire ?

Avant d'aborder la mémoire en JavaScript, nous discuterons brièvement de ce qu'est la mémoire et de son fonctionnement.

Au niveau matériel, la mémoire de l'ordinateur est mise en cache par un grand nombre de déclencheurs. Chaque bascule contient plusieurs transistors capables de stocker un bit, et les bascules individuelles sont adressables par un identifiant unique afin que nous puissions les lire et les écraser. Par conséquent, sur le plan conceptuel, la mémoire entière de l’ordinateur peut être considérée comme un vaste ensemble pouvant être lu et écrit.

En tant qu'humains, nous ne sommes pas très doués pour penser et calculer en termes de bits, nous les organisons donc en groupes plus grands qui, ensemble, peuvent être utilisés pour représenter des nombres. 8 bits sont appelés 1 octet. En plus des octets, il existe également des mots (parfois 16 bits, parfois 32 bits).

Beaucoup de choses sont stockées en mémoire :

  1. Toutes les variables et autres données utilisées par le programme.
  2. Code du programme, y compris le code du système d'exploitation.

Le compilateur et le système d'exploitation gèrent l'essentiel de la gestion de la mémoire pour vous, mais vous devez toujours comprendre la situation sous-jacente pour avoir une compréhension plus approfondie des concepts de gestion sous-jacents.

Lors de la compilation du code, le compilateur peut vérifier les types de données de base et calculer à l'avance la quantité de mémoire dont elles ont besoin. La taille requise est ensuite allouée au programme dans l'espace de la pile d'appels, l'espace où ces variables sont allouées est appelé espace de pile. Parce que lorsque les fonctions sont appelées, leur mémoire est ajoutée à la mémoire existante et lorsqu'elles se terminent, elles sont supprimées dans l'ordre du dernier entré, premier sorti (LIFO). Par exemple :

Introduction à la gestion de la mémoire JavaScript + comment gérer 4 fuites de mémoire courantes

Le compilateur connaît immédiatement la mémoire requise : 4 + 4×4 + 8 = 28 octets.

Ce code montre la taille de la mémoire occupée par les variables entières et à virgule flottante double précision. Mais il y a environ 20 ans, les variables entières occupaient généralement 2 octets, tandis que les variables à virgule flottante double précision occupaient 4 octets. Votre code ne doit pas dépendre de la taille du type de données de base actuel.

Le compilateur insérera du code qui interagit avec le système d'exploitation et allouera le nombre d'octets de pile nécessaires pour stocker les variables.

Dans l'exemple ci-dessus, le compilateur connaît l'adresse mémoire exacte de chaque variable. En fait, chaque fois que nous écrivons dans la variable n, elle est convertie en interne en quelque chose comme “内存地址4127963”.

Notez que si nous essayons d'accéder à x[4], les données associées à m seront accessibles. En effet, accéder à un élément inexistant dans le tableau (qui est 4 octets plus grand que le dernier élément réellement alloué dans le tableau, x[3]), peut finir par lire (ou écraser) quelques m bits. Cela aura certainement des résultats imprévisibles sur le reste du programme.

Introduction à la gestion de la mémoire JavaScript + comment gérer 4 fuites de mémoire courantes

Lorsque des fonctions appellent d'autres fonctions, chaque fonction obtient son propre bloc sur la pile d'appels. Il contient toutes les variables locales, mais dispose également d'un compteur de programme pour se rappeler où il se trouve pendant l'exécution. Une fois la fonction terminée, son bloc mémoire est à nouveau utilisé ailleurs.

Allocation dynamique

Malheureusement, les choses se compliquent un peu quand on ne sait pas au moment de la compilation combien de mémoire une variable nécessite. Supposons que nous voulions faire ce qui suit :

Introduction à la gestion de la mémoire JavaScript + comment gérer 4 fuites de mémoire courantes

Au moment de la compilation, le compilateur ne sait pas combien de mémoire le tableau doit utiliser car cela est déterminé par la valeur fournie par le utilisateur.

Par conséquent, il ne peut pas allouer d'espace pour les variables sur la pile. Au lieu de cela, notre programme doit demander explicitement l'espace approprié au système d'exploitation au moment de l'exécution. Cette mémoire est allouée à partir de l'espace tas. La différence entre l'allocation de mémoire statique et l'allocation de mémoire dynamique est résumée dans le tableau suivant :

静态内存分配 动态内存分配
大小必须在编译时知道 大小不需要在编译时知道
在编译时执行 在运行时执行
分配给堆栈 分配给堆
FILO (先进后出) 没有特定的分配顺序

Pour bien comprendre le fonctionnement de l'allocation dynamique de mémoire, vous devez consacrer plus de temps aux pointeurs, ce qui peut trop s'écarter du sujet de cet article. Je ne présenterai pas ici en détail les connaissances associées aux pointeurs.

Allocation de mémoire en JavaScript

Maintenant, la première étape va être expliquée : Comment allouer de la mémoire en JavaScript.

JavaScript décharge les développeurs de la responsabilité de gérer manuellement l'allocation de mémoire - JavaScript alloue de la mémoire et déclare les valeurs par lui-même.

Introduction à la gestion de la mémoire JavaScript + comment gérer 4 fuites de mémoire courantes

Certains appels de fonctions entraînent également une allocation de mémoire d'objets :

Introduction à la gestion de la mémoire JavaScript + comment gérer 4 fuites de mémoire courantes

les méthodes peuvent allouer de nouvelles valeurs ou de nouveaux objets :

Introduction à la gestion de la mémoire JavaScript + comment gérer 4 fuites de mémoire courantes

Utiliser la mémoire en JavaScript

Utiliser la mémoire allouée en JavaScript signifie lire et écrire dedans, cela peut être fait en lisant ou en écrivant des variables ou la valeur d'une propriété d'objet, ou en passant des paramètres à une fonction.

Libérer la mémoire lorsqu'elle n'est plus nécessaire

La plupart des problèmes de gestion de la mémoire surviennent à ce stade

La partie la plus difficile ici est de déterminer quand l'allocation de mémoire n'est plus nécessaire , cela nécessite généralement que le développeur identifie où dans le programme la mémoire n'est plus nécessaire et la libère.

Les langages de haut niveau intègrent un mécanisme appelé garbage collector, dont le travail consiste à suivre l'allocation et l'utilisation de la mémoire afin de détecter à chaque fois qu'un morceau de mémoire alloué n'est plus nécessaire. Dans ce cas, il libérera automatiquement cette mémoire.

Malheureusement, ce processus n'est qu'une estimation approximative, car il est difficile de savoir si un certain bloc de mémoire est vraiment nécessaire (ne peut pas être résolu algorithmiquement).

La plupart des garbage collector fonctionnent en collectant la mémoire qui n'est plus accessible, c'est-à-dire que toutes les variables pointant vers elle sont devenues hors de portée. Cependant, il s'agit d'une sous-estimation de l'ensemble de l'espace mémoire qui peut être collecté, car à tout moment dans un emplacement mémoire, il peut toujours y avoir une variable dans sa portée pointant vers lui, mais elle ne sera plus jamais accessible.

Garbage Collection

Comme il est impossible de déterminer si de la mémoire est vraiment utile, le garbage collector a pensé à un moyen de résoudre ce problème. Cette section explique et comprend les principaux algorithmes de garbage collection et leurs limites.

Référence de mémoire

L'algorithme de récupération de place repose principalement sur des références.

Dans le contexte de la gestion de la mémoire, un objet est dit faire référence à un autre objet s'il a accès à un autre objet (soit implicitement, soit explicitement). Par exemple, un objet JavaScript comporte des références à son prototype (référence implicite) et des valeurs de propriété (référence explicite).

Dans ce contexte, le concept d'« objet » est élargi pour être plus large que les objets JavaScript classiques, et inclut également la portée des fonctions (ou portée lexicale globale).

La portée lexicale définit la façon dont les noms de variables sont résolus dans les fonctions imbriquées : les fonctions internes contiennent les effets de la fonction parent même si la fonction parent a renvoyé

Algorithme de garbage collection de comptage de références

Il s'agit de l'algorithme de garbage collection le plus simple. S'il n'y a aucune référence à l'objet, l'objet est considéré comme "garbage collectable", comme dans le code suivant :

Introduction à la gestion de la mémoire JavaScript + comment gérer 4 fuites de mémoire courantes

Les boucles peuvent causer des problèmes

quand En ce qui concerne la boucle, il y a une limite. Dans l'exemple ci-dessous, deux objets sont créés et se référencent mutuellement, créant ainsi une boucle. Une fois que l'appel de fonction sera hors de portée, il sera effectivement inutile et pourra être libéré. Cependant, l'algorithme de comptage de références estime que puisque chaque objet a été référencé au moins une fois, aucun d'entre eux ne peut être récupéré.

Introduction à la gestion de la mémoire JavaScript + comment gérer 4 fuites de mémoire courantes

Algorithme de marquage et de balayage

Cet algorithme peut déterminer si un objet est accessible, sachant ainsi si l'objet est utile, l'algorithme se compose de les étapes suivantes :

  1. Le garbage collector construit une liste "racine" qui contient les variables globales référencées. En JavaScript, l'objet « window » est une variable globale qui peut être utilisée comme nœud racine.
  2. L'algorithme vérifie ensuite toutes les racines et leurs enfants et les marque comme actives (ce qui signifie qu'elles ne sont pas des déchets). Tout endroit que les racines ne peuvent pas atteindre sera marqué comme déchet.
  3. Enfin, le garbage collector libère tous les blocs de mémoire qui ne sont pas marqués comme actifs et renvoie cette mémoire au système d'exploitation.

Introduction à la gestion de la mémoire JavaScript + comment gérer 4 fuites de mémoire courantes

Cet algorithme est meilleur que l'algorithme précédent, car "un objet n'est pas référencé" signifie que l'objet n'est pas accessible.

Depuis 2012, tous les navigateurs modernes disposent de garbage collector qui effacent les marques. Toutes les améliorations apportées dans le domaine du garbage collection JavaScript (collecte des ordures générationnelle/incrémentale/concurrente/parallèle) au cours des dernières années ont été des améliorations de la mise en œuvre de l'algorithme (mark-sweep), et non des améliorations de l'algorithme de garbage collection lui-même. est-ce le but de déterminer si un objet est accessible.

Dans cet article, vous pouvez en savoir plus sur le suivi du garbage collection, y compris l'algorithme de marquage et ses optimisations.

Le bouclage n'est plus un problème

Dans le premier exemple ci-dessus, après le retour de l'appel de fonction, les deux objets ne sont plus référencés par des objets accessibles depuis l'objet global. Par conséquent, le ramasse-miettes les trouvera inaccessibles.

Introduction à la gestion de la mémoire JavaScript + comment gérer 4 fuites de mémoire courantes

Bien qu'il existe des références entre les objets, elles ne sont pas accessibles depuis le nœud racine.

Comportement contre-intuitif des éboueurs

Bien que les éboueurs soient pratiques, ils ont leur propre ensemble de compromis, dont l'un est le non-déterminisme. En d'autres termes, GC ne l'est pas. Comme on pouvait s'y attendre, vous ne pouvez pas vraiment dire quand la collecte des ordures aura lieu. Cela signifie que dans certains cas, le programme utilisera plus de mémoire que ce qui est réellement nécessaire. Dans les applications particulièrement sensibles à la vitesse, de courtes pauses peuvent être perceptibles. Si aucune mémoire n'est allouée, la majeure partie du GC sera inactive. Jetez un œil au scénario suivant :

  1. Allouez un ensemble assez important d'internes.
  2. La plupart (ou la totalité) de ces éléments sont marqués comme inaccessibles (en supposant que la référence pointe vers un cache qui n'est plus nécessaire).
  3. Aucune autre allocation

Dans ces scénarios, la plupart des GC ne continueront pas à être collectés. Autrement dit, même s’il existe des références inaccessibles disponibles à la collecte, le collectionneur ne les déclarera pas. Il ne s’agit pas strictement de fuites, mais elles peuvent néanmoins entraîner une utilisation de la mémoire plus élevée que d’habitude.

Qu'est-ce qu'une fuite de mémoire ?

Essentiellement, une fuite de mémoire peut être définie comme : de la mémoire qui n'est plus nécessaire à l'application et, pour une raison quelconque, ne revient pas au système d'exploitation. ou un pool de mémoire libre.

Introduction à la gestion de la mémoire JavaScript + comment gérer 4 fuites de mémoire courantes

Les langages de programmation prennent en charge différentes méthodes de gestion de la mémoire. Cependant, la question de savoir s’il faut ou non utiliser une certaine partie de la mémoire est en réalité une question indéterminée. En d’autres termes, seul le développeur peut dire si un morceau de mémoire peut être restitué au système d’exploitation.

Certains langages de programmation fournissent une assistance aux développeurs, d'autres attendent des développeurs qu'ils comprennent clairement quand la mémoire n'est plus utilisée. Wikipedia propose d'excellents articles sur la gestion manuelle et automatique de la mémoire.

Quatre fuites de mémoire courantes

1. Variables globales

JavaScript gère les variables non déclarées de manière intéressante : Pour les variables non déclarées, une nouvelle variable sera créé dans la portée globale pour le référencer. Dans un navigateur, l'objet global est window. Par exemple :

function foo(arg) {
    bar = "some text";
}

est équivalent à :

function foo(arg) {
    window.bar = "some text";
}

Si bar fait référence à une variable dans la portée de la fonction foo mais oublie d'utiliser var pour la déclarer, un global inattendu sera créé. variable. Dans cet exemple, manquer une simple chaîne ne ferait pas beaucoup de mal, mais ce serait certainement mauvais.

Une autre façon de créer une variable globale inattendue est d'utiliser ceci :

function foo() {
    this.var1 = "potential accidental global";
}
// Foo自己调用,它指向全局对象(window),而不是未定义。
foo();
Vous pouvez éviter cela en ajoutant "use strict" au début de votre fichier JavaScript et cela activera une variable plus stricte. Mode d'analyse JavaScript pour empêcher la création accidentelle de variables globales.

Bien que nous parlions de variables globales inconnues, il existe encore beaucoup de code rempli de variables globales explicites. Par définition, ceux-ci ne sont pas collectables (sauf si spécifiés comme vides ou réaffectés). Les variables globales, utilisées pour stocker et traiter temporairement de grandes quantités d’informations, sont particulièrement préoccupantes. Si vous devez utiliser une variable globale pour stocker de grandes quantités de données, veillez à spécifier null ou à la réaffecter lorsque vous avez terminé.

2. Minuteries et rappels oubliés

Prenez setInterval comme exemple car il est souvent utilisé en JavaScript.

var serverData = loadData();
setInterval(function() {
    var renderer = document.getElementById('renderer');
    if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
    }
}, 5000); //每五秒会执行一次

L'extrait de code ci-dessus montre l'utilisation d'un minuteur pour référencer des nœuds ou des données qui ne sont plus nécessaires. L'objet représenté par

renderer peut être supprimé à un moment donné dans le futur, ce qui rendra inutile un bloc entier de code dans le gestionnaire interne. Cependant, comme le timer est toujours actif, le gestionnaire ne peut pas être collecté et ses dépendances ne peuvent pas être collectées. Cela signifie que serverData, qui stocke de grandes quantités de données, ne peut pas être collecté.

Lorsque vous utilisez des observateurs, vous devez vous assurer que vous effectuez un appel explicite pour les supprimer une fois que vous avez fini de les utiliser (soit l'observateur n'est plus nécessaire, soit l'objet deviendra inaccessible).

En tant que développeur, vous devez vous assurer de les supprimer explicitement lorsque vous en avez terminé avec eux (sinon l'objet sera inaccessible).

Dans le passé, certains navigateurs ne pouvaient pas gérer ces situations (sympa IE6). Heureusement, la plupart des navigateurs modernes le font désormais pour vous : ils collectent automatiquement les gestionnaires d'observateurs une fois que l'objet observé devient inaccessible, même si vous oubliez de supprimer l'écouteur. Cependant, nous devons toujours supprimer explicitement ces observateurs avant que l'objet ne soit supprimé. Par exemple :

Introduction à la gestion de la mémoire JavaScript + comment gérer 4 fuites de mémoire courantes

Les navigateurs actuels (y compris IE et Edge) utilisent des algorithmes modernes de collecte des déchets qui peuvent détecter et gérer ces références circulaires immédiatement. En d’autres termes, il n’est pas nécessaire d’appeler removeEventListener avant qu’un nœud ne soit supprimé.

Certains frameworks ou bibliothèques, tels que JQuery, supprimeront automatiquement les écouteurs avant de supprimer le nœud (lors de l'utilisation de leur API spécifique). Ceci est implémenté par un mécanisme au sein de la bibliothèque qui garantit qu'aucune fuite de mémoire ne se produit, même lors de l'exécution dans des navigateurs problématiques, tels que... IE 6.

3. Fermeture

La fermeture est un aspect clé du développement JavaScript. Une fonction interne utilise les variables d'une fonction externe (fermée). En raison des détails de l'exécution de JavaScript, il peut provoquer une fuite de mémoire de la manière suivante :

Introduction à la gestion de la mémoire JavaScript + comment gérer 4 fuites de mémoire courantes

Ce code fait une chose : chaque fois que replaceThing est appelé, 🎜 >Vous obtiendrez un nouvel objet contenant un grand tableau et une nouvelle fermeture (someMethod). En même temps, la variable theThingd pointe vers une fermeture qui fait référence à unuse. `originalThing

Confondu ? L'important est qu'une fois qu'une portée avec plusieurs fermetures avec la même portée parent est créée, la portée peut être partagée.

Dans ce cas, le périmètre créé pour la fermeture

peut être partagé par someMethod. Il y a une référence à unused à l'intérieur de unused. Même si originalThing n'est jamais utilisé, unused peut être appelé depuis someMethod en dehors de la portée de replaceThing (par exemple, dans la portée globale). theThing

Puisque

partage la portée de la someMethod fermeture, alors unused référencer le unused contenant le force à rester actif (la totalité de la portée partagée entre les deux fermetures). Cela empêche sa collecte. originalThing

Lorsque ce code est exécuté à plusieurs reprises, vous pouvez observer que l'utilisation de la mémoire augmente régulièrement. Lorsque

est exécuté, l'utilisation de la mémoire ne diminuera pas. Essentiellement, une liste chaînée de fermeture est créée pendant l'exécution (sa racine existe sous la forme d'une variable GC), et la portée de chaque fermeture fait référence indirectement à un grand tableau, ce qui provoque une fuite de mémoire assez importante. theThing

4. Références détachées du DOM

Parfois, il peut être utile de stocker des nœuds DOM dans une structure de données. Supposons que vous souhaitiez mettre à jour rapidement plusieurs lignes d'un tableau, vous pouvez alors enregistrer une référence à chaque ligne DOM dans un dictionnaire ou un tableau. De cette façon, il existe deux références au même élément DOM : une dans l’arborescence DOM et l’autre dans le dictionnaire. Si, à un moment donné dans le futur, vous décidez de supprimer ces lignes, vous devrez alors rendre les deux références inaccessibles.

Il y a un autre problème à prendre en compte lors du référencement de nœuds internes ou de nœuds feuilles dans l'arborescence DOM. Si vous conservez une référence à une cellule du tableau (balise ) dans votre code et décidez de supprimer le tableau du DOM tout en conservant une référence à cette cellule spécifique, une fuite de mémoire peut se produire.

Vous pourriez penser que le ramasse-miettes libérera tout sauf cette cellule. Cependant, ce n'est pas le cas, puisque la cellule est un nœud enfant de la table, et que les nœuds enfants contiennent des références aux nœuds parents, cette référence à la cellule du tableau gardera

la table entière en mémoire , donc quand en supprimant un nœud référencé, supprimez ses nœuds enfants.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer