Maison  >  Article  >  interface Web  >  Introduction détaillée au code graphique des fuites de mémoire dans les programmes JavaScript

Introduction détaillée au code graphique des fuites de mémoire dans les programmes JavaScript

黄舟
黄舟original
2017-03-09 14:11:011450parcourir

Introduction détaillée du code graphique des fuites de mémoire dans les programmes JavaScript :

Le garbage collection nous libère, il nous permet de nous concentrer sur la logique applicative (pas de gestion de la mémoire). Cependant, la collecte des déchets n’a rien de magique. Comprendre comment cela fonctionne et comment lui faire conserver la mémoire qui aurait dû être libérée depuis longtemps peut conduire à des applications plus rapides et plus fiables. Dans cet article, découvrez une approche systématique pour localiser les fuites de mémoire dans les applications JavaScript, plusieurs modèles de fuite courants et les méthodes appropriées pour résoudre ces fuites.

Introduction

Lorsqu'il s'agit d'un langage de script tel que JavaScript, il est facile d'oublier que chaque objet, classe, chaîne, nombre et méthode doit allouer et réserver de la mémoire. Les détails spécifiques de l'allocation et de la désallocation de mémoire sont masqués dans les garbage collector du langage et de l'exécution.

De nombreuses fonctions peuvent être implémentées sans tenir compte de la gestion de la mémoire, mais l'ignorer peut entraîner des problèmes importants dans le programme. Les objets mal nettoyés peuvent persister beaucoup plus longtemps que prévu. Ces objets continuent de répondre aux événements et de consommer des ressources. Ils obligent le navigateur à allouer des pages de mémoire à partir d'un lecteur de disque virtuel, ce qui ralentit considérablement l'ordinateur (et dans les cas extrêmes, provoque le crash du navigateur).

Une fuite de mémoire est tout objet qui persiste après que vous ne le possédez plus ou n'en avez plus besoin. Ces dernières années, de nombreux navigateurs ont amélioré leur capacité à récupérer la mémoire de JavaScript lors du chargement des pages. Cependant, tous les navigateurs ne fonctionnent pas de la même manière. Firefox et les anciennes versions d'Internet Explorer ont connu des fuites de mémoire qui persistent jusqu'à la fermeture du navigateur.

De nombreux modèles classiques qui provoquaient des fuites de mémoire dans le passé ne provoquent plus de fuites de mémoire dans les navigateurs modernes. Cependant, il existe aujourd’hui une tendance différente affectant les fuites de mémoire. Beaucoup conçoivent des applications Web pour qu’elles s’exécutent sur une seule page sans actualisation matérielle des pages. Dans une seule page comme celle-là, il est facile de conserver la mémoire qui n'est plus nécessaire ou pertinente lors du passage d'un état de l'application à un autre.

Dans cet article, découvrez le cycle de vie de base des objets, comment le garbage collection détermine si un objet a été libéré et comment évaluer le comportement potentiel en matière de fuite. Découvrez également comment utiliser Heap Profiler dans Google Chrome pour diagnostiquer les problèmes de mémoire. Quelques exemples montrent comment résoudre les fuites de mémoire causées par des fermetures, des journaux de console et des boucles.

Cycle de vie des objets

Pour comprendre comment éviter les fuites de mémoire, vous devez comprendre le cycle de vie de base des objets. Lorsqu'un objet est créé, JavaScript alloue automatiquement la mémoire appropriée pour l'objet. À partir de ce moment, le garbage collector évalue en permanence l'objet pour voir s'il s'agit toujours d'un objet valide.

Le ramasse-miettes analyse périodiquement les objets et compte le nombre d'autres objets qui ont des références à chaque objet. Si un objet a 0 référence (aucun autre objet n'y fait référence) ou si la seule référence à l'objet est circulaire, alors la mémoire de l'objet peut être récupérée. La figure 1 montre un exemple de récupération de mémoire par le garbage collector.

Figure 1. Récupération de mémoire via le garbage collection

展示与各个对象关联的 root 节点的 4 个步骤。

Il serait utile de voir ce système en action, mais les outils qui fournissent cette fonctionnalité sont limités. Une façon de connaître la quantité de mémoire utilisée par votre application JavaScript consiste à utiliser les outils système pour examiner les allocations de mémoire de votre navigateur. Il existe plusieurs outils qui peuvent vous fournir des graphiques d'utilisation actuelle et de tendance de l'utilisation de la mémoire d'un processus au fil du temps.

Par exemple, si XCode est installé sur Mac OSX, vous pouvez lancer l'application Instruments et attacher son outil Activity Monitor à votre navigateur pour une analyse en temps réel. Sous Windows®, vous pouvez utiliser le Gestionnaire des tâches. Vous savez qu'il y a une fuite de mémoire si vous remarquez une augmentation constante de l'utilisation de la mémoire au fil du temps à mesure que vous utilisez votre application.

L'observation de l'empreinte mémoire du navigateur ne donne qu'une indication très approximative de l'utilisation réelle de la mémoire par une application JavaScript. Les données du navigateur ne vous indiquent pas quels objets ont été divulgués et rien ne garantit que les données correspondent réellement à la véritable empreinte mémoire de votre application. De plus, en raison de problèmes d'implémentation dans certains navigateurs, les éléments DOM (ou d'autres objets au niveau de l'application) peuvent ne pas être publiés lorsque l'élément correspondant est détruit dans la page. Cela est particulièrement vrai pour les balises vidéo, qui nécessitent que les navigateurs mettent en œuvre une infrastructure plus élaborée.

Il y a eu plusieurs tentatives pour ajouter un suivi des allocations de mémoire dans les bibliothèques JavaScript côté client. Malheureusement, aucune des tentatives n’était particulièrement fiable. Par exemple, le package stats.js populaire n'est pas pris en charge en raison d'inexactitudes. En général, essayer de conserver ou de déterminer ces informations à partir du client est problématique car cela introduit une surcharge dans l'application et ne peut pas être terminé de manière fiable.

La solution idéale serait que le fournisseur du navigateur fournisse un ensemble d'outils dans le navigateur qui vous aident à surveiller l'utilisation de la mémoire, à identifier les objets divulgués et à déterminer pourquoi un objet particulier est toujours marqué comme réservé.

Actuellement, seul Google Chrome (qui fournit Heap Profile) implémente un outil de gestion de la mémoire comme outils de développement. Dans cet article, j'utilise Heap Profiler pour tester et démontrer comment le runtime JavaScript gère la mémoire.

Analyse des instantanés de tas

Avant de créer une fuite de mémoire, examinez une interaction simple qui collecte correctement la mémoire. Commencez par créer une simple page HTML avec deux boutons, comme indiqué dans le listing 1.

Listing 1. index.html

<html>
<head>
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js" 
type="text/javascript"></script>
</head>
<body>
    <button id="start_button">Start</button>
    <button id="destroy_button">Destroy</button>
    <script src="assets/scripts/leaker.js" type="text/javascript" 
charset="utf-8"></script>
    <script src="assets/scripts/main.js" type="text/javascript" 
charset="utf-8"></script>
</body>
</html>

L'inclusion de jQuery vise à garantir une syntaxe simple pour gérer les liaisons d'événements adaptées à différents navigateurs, et adhérer strictement aux pratiques de développement les plus courantes. Ajoutez des balises de script à la classe leaker et aux principales méthodes JavaScript. Dans un environnement de développement, il est souvent préférable de combiner des fichiers JavaScript en un seul fichier. Pour les besoins de cet exemple, il est plus facile de placer la logique dans un fichier séparé.

Vous pouvez filtrer le Heap Profiler pour afficher uniquement les instances d'une classe particulière. Pour profiter de cette fonctionnalité, créez une nouvelle classe qui encapsule le comportement des objets qui fuient, et cette classe se trouve facilement dans le Heap Profiler, comme indiqué dans le listing 2.

Listing 2. assets/scripts/leaker.js

var Leaker = function(){};
Leaker.prototype = {
    init:function(){

    }    
};

Liez le bouton Démarrer pour initialiser l'objet Leaker et ajouter Il est affecté à une variable dans l'espace de noms global. Vous devez également lier le bouton Détruire à une méthode qui doit nettoyer l'objet Leaker et le préparer pour le garbage collection, comme indiqué dans le listing 3.

Listing 3. assets/scripts/main.js

$("#start_button").click(function(){
    if(leak !== null || leak !== undefined){
        return;
    }
  leak = new Leaker();
  leak.init();
});

$("#destroy_button").click(function(){
    leak = null;
});

var leak = new Leaker();

Vous êtes maintenant prêt à créer un objet en mémoire Regardez-le puis relâchez-le.

  1. Chargez la page d'index dans Chrome. Étant donné que vous chargez jQuery directement depuis Google, vous aurez besoin d'une connexion Internet pour exécuter cet exemple.

  2. Ouvrez les outils de développement en ouvrant le menu Affichage et en sélectionnant le sous-menu Développer. Sélectionnez la commande Outils de développement.

  3. Accédez à l'onglet Profils et prenez un instantané du tas, comme indiqué dans la figure 2.

    Figure 2. Onglet Profils

    Google Chrome 上的 profiles 选项卡的快照。

  4. Reportez votre attention sur le Web et sélectionnez Démarrer .

  5. Prenez un autre instantané du tas.

  6. Filtrez le premier instantané et recherchez les instances de la classe Leaker, et aucune instance n'est trouvée. Passez au deuxième instantané et vous devriez trouver une instance comme le montre la figure 3.

    Figure 3. Exemple d'instantané

    Heap Profiler 过滤器页面的快照

  7. Reportez votre attention sur le Web et sélectionnez Détruire .

  8. Prenez le troisième instantané du tas.

  9. Filtrez le troisième instantané et recherchez les instances de la classe Leaker, et aucune instance n'est trouvée. Lors du chargement du troisième instantané, vous pouvez également basculer le mode d'analyse de Résumé à Comparaison et comparer les troisième et deuxième instantanés. Vous verrez une valeur de décalage de -1 (une instance de l'objet Leaker a été publiée entre les instantanés).

Vive la vie ! La collecte des déchets est efficace. Il est maintenant temps de le détruire.

Fuite de mémoire 1 : fermetures

Un moyen simple d'empêcher qu'un objet soit récupéré est de définir un intervalle ou un délai d'attente pour référencer l'objet dans les rappels. Pour le voir en action, mettez à jour la classe leaker.js, comme indiqué dans le listing 4.

Listing 4. assets/scripts/leaker.js

var Leaker = function(){};

Leaker.prototype = {
    init:function(){
        this._interval = null;
        this.start();
    },

    start: function(){
        var self = this;
        this._interval = setInterval(function(){
            self.onInterval();
        }, 100);
    },

    destroy: function(){
        if(this._interval !== null){
            clearInterval(this._interval);          
        }
    },

    onInterval: function(){
        console.log("Interval");
    }
};

Maintenant, en répétant les étapes 1 à 9 de la section précédente Quand vous faites un pas, vous devriez voir dans le troisième instantané que l'objet Leaker est persistant et que l'intervalle continue de s'exécuter pour toujours. Alors que s'est-il passé ? Toutes les variables locales référencées dans une fermeture sont conservées par la fermeture aussi longtemps que la fermeture existe. Pour garantir que le rappel de la méthode setInterval est exécuté lors de l'accès à la portée de l'instance Leaker, la variable this doit être affectée à la variable locale self, qui est utilisée pour déclencher onInterval depuis l'intérieur la fermeture. Lorsque onInterval se déclenche, il peut accéder à n'importe quelle variable d'instance dans l'objet Leaker (y compris lui-même). Cependant, tant que l'écouteur d'événement existe, l'objet Leaker ne sera pas récupéré.

Pour résoudre ce problème, déclenchez la méthode leaker ajoutée à l'objet destroy stocké avant d'effacer l'objet en mettant à jour le gestionnaire de clic du bouton Détruire, comme indiqué dans le Listing 5 Show.

Liste 5. actifs/scripts/main.js

$("#destroy_button").click(function(){
    leak.destroy();
    leak = null;
});

销毁对象和对象所有权

一种不错的做法是,创建一个标准方法来负责让一个对象有资格被垃圾回收。destroy 功能的主要用途是,集中清理该对象完成的具有以下后果的操作的职责:

  • 阻止它的引用计数下降到 0(例如,删除存在问题的事件侦听器和回调,并从任何服务取消注册)。

  • 使用不必要的 CPU 周期,比如间隔或动画。

destroy 方法常常是清理一个对象的必要步骤,但在大多数情况下它还不够。在理论上,在销毁相关实例后,保留对已销毁对象的引用的其他对象可调用自身之上的方法。因为这种情形可能会产生不可预测的结果,所以仅在对象即将无用时调用 destroy 方法,这至关重要。

一般而言,destroy 方法最佳使用是在一个对象有一个明确的所有者来负责它的生命周期时。此情形常常存在于分层系统中,比如 MVC 框架中的视图或控制器,或者一个画布呈现系统的场景图。

内存泄漏 2:控制台日志

一种将对象保留在内存中的不太明显的方式是将它记录到控制台中。清单 6 更新了 Leaker 类,显示了此方式的一个示例。

清单 6. assets/scripts/leaker.js

var Leaker = function(){};

Leaker.prototype = {
    init:function(){
        console.log("Leaking an object: %o", this);
    },

    destroy: function(){

    }      
};

可采取以下步骤来演示控制台的影响。

  1. 登录到索引页面。

  2. 单击 Start

  3. 转到控制台并确认 Leaking 对象已被跟踪。

  4. 单击 Destroy

  5. 回到控制台并键入 leak,以记录全局变量当前的内容。此刻该值应为空。

  6. 获取另一个堆快照并过滤 Leaker 对象。您应留下一个 Leaker 对象。

  7. 回到控制台并清除它。

  8. 创建另一个堆配置文件。在清理控制台后,保留 leaker 的配置文件应已清除。

控制台日志记录对总体内存配置文件的影响可能是许多开发人员都未想到的极其重大的问题。记录错误的对象可以将大量数据保留在内存中。注意,这也适用于:

  • 在用户键入 JavaScript 时,在控制台中的一个交互式会话期间记录的对象。

  • 由 console.log 和 console.dir 方法记录的对象。

内存泄漏 3:循环

在两个对象彼此引用且彼此保留时,就会产生一个循环,如图 4 所示。

图 4. 创建一个循环的引用

该图中的一个蓝色 root 节点连接到两个绿色框,显示了它们之间的一个连接

清单 7 显示了一个简单的代码示例。

清单 7. assets/scripts/leaker.js

var Leaker = function(){};

Leaker.prototype = {
    init:function(name, parent){
        this._name = name;
        this._parent = parent;
        this._child = null;
        this.createChildren();
    },

    createChildren:function(){
        if(this._parent !== null){
            // Only create a child if this is the root
            return;
        }
        this._child = new Leaker();
        this._child.init("leaker 2", this);
    },

    destroy: function(){

    }
};

Root 对象的实例化可以修改,如清单 8 所示。

清单 8. assets/scripts/main.js

leak = new Leaker(); 
leak.init("leaker 1", null);

如果在创建和销毁对象后执行一次堆分析,您应该会看到垃圾收集器检测到了这个循环引用,并在您选择 Destroy 按钮时释放了内存。

但是,如果引入了第三个保留该子对象的对象,该循环会导致内存泄漏。例如,创建一个 registry 对象,如清单 9 所示。

清单 9. assets/scripts/registry.js

var Registry = function(){};

Registry.prototype = {
    init:function(){
        this._subscribers = [];
    },

    add:function(subscriber){
        if(this._subscribers.indexOf(subscriber) >= 0){
            // Already registered so bail out
            return;
        }
        this._subscribers.push(subscriber);
    },

    remove:function(subscriber){
        if(this._subscribers.indexOf(subscriber) < 0){
            // Not currently registered so bail out
            return;
        }
              this._subscribers.splice(
                  this._subscribers.indexOf(subscriber), 1
              );
    }
};

registry 类是让其他对象向它注册,然后从注册表中删除自身的对象的简单示例。尽管这个特殊的类与注册表毫无关联,但这是事件调度程序和通知系统中的一种常见模式。

将该类导入 index.html 页面中,放在 leaker.js 之前,如清单 10 所示。

清单 10. index.html

<script src="assets/scripts/registry.js" type="text/javascript" 
charset="utf-8"></script>

更新 Leaker 对象,以向注册表对象注册该对象本身(可能用于有关一些未实现事件的通知)。这创建了一个来自要保留的 leaker 子对象的 root 节点备用路径,但由于该循环,父对象也将保留,如清单 11 所示。

清单 11. assets/scripts/leaker.js

var Leaker = function(){};
Leaker.prototype = {

    init:function(name, parent, registry){
        this._name = name;
        this._registry = registry;
        this._parent = parent;
        this._child = null;
        this.createChildren();
        this.registerCallback();
    },

    createChildren:function(){
        if(this._parent !== null){
            // Only create child if this is the root
            return;
        }
        this._child = new Leaker();
        this._child.init("leaker 2", this, this._registry);
    },

    registerCallback:function(){
        this._registry.add(this);
    },

    destroy: function(){
        this._registry.remove(this);
    }
};

最后,更新 main.js 以设置注册表,并将对注册表的一个引用传递给 leaker 父对象,如清单 12 所示。

清单 12. assets/scripts/main.js

	  $("#start_button").click(function(){
  var leakExists = !(
	      window["leak"] === null || window["leak"] === undefined
	  );
  if(leakExists){
      return;
  }
  leak = new Leaker();
  leak.init("leaker 1", null, registry);
});

$("#destroy_button").click(function(){
    leak.destroy();
    leak = null;
});

registry = new Registry();
registry.init();

现在,当执行堆分析时,您应看到每次选择 Start 按钮时,会创建并保留 Leaker 对象的两个新实例。图 5 显示了对象引用的流程。

图 5. 由于保留引用导致的内存泄漏

3 个方框显示了 root 节点与父和子对象之间的 3 个不同路径

从表面上看,它像一个不自然的示例,但它实际上非常常见。更加经典的面向对象框架中的事件侦听器常常遵循类似图 5 的模式。这种类型的模式也可能与闭包和控制台日志导致的问题相关联。

尽管有多种方式来解决此类问题,但在此情况下,最简单的方式是更新 Leaker 类,以在销毁它时销毁它的子对象。对于本示例,更新destroy 方法(如清单 13 所示)就足够了。

清单 13. assets/scripts/leaker.js

destroy: function(){
    if(this._child !== null){
        this._child.destroy();            
    }
    this._registry.remove(this);
}

有时,两个没有足够紧密关系的对象之间也会存在循环,其中一个对象管理另一个对象的生命周期。在这样的情况下,在这两个对象之间建立关系的对象应负责在自己被销毁时中断循环。

结束语

即使 JavaScript 已被垃圾回收,仍然会有许多方式会将不需要的对象保留在内存中。目前大部分浏览器都已改进了内存清理功能,但评估您应用程序内存堆的工具仍然有限(除了使用 Google Chrome)。通过从简单的测试案例开始,很容易评估潜在的泄漏行为并确定是否存在泄漏。

不经过测试,就不可能准确度量内存使用。很容易使循环引用占据对象曲线图中的大部分区域。Chrome 的 Heap Profiler 是一个诊断内存问题的宝贵工具,在开发时定期使用它也是一个不错的选择。在预测对象曲线图中要释放的具体资源时请设定具体的预期,然后进行验证。任何时候当您看到不想要的结果时,请仔细调查。

在创建对象时要计划该对象的清理工作,这比在以后将一个清理阶段移植到应用程序中要容易得多。常常要计划删除事件侦听器,并停止您创建的间隔。如果认识到了您应用程序中的内存使用,您将得到更可靠且性能更高的应用程序。


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:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn