Maison >interface Web >js tutoriel >Compréhension approfondie des fuites de mémoire dans les programmes JavaScript_compétences Javascript

Compréhension approfondie des fuites de mémoire dans les programmes JavaScript_compétences Javascript

WBOY
WBOYoriginal
2016-05-16 15:10:191151parcourir

Le garbage collection nous permet de nous concentrer sur la logique des applications (plutôt que sur la 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.

1. Introduction

Lorsque vous utilisez 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 de fuite potentiel. 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 dues aux fermetures, aux journaux de console et aux boucles.

2. 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 garbage collector 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 grâce au garbage collection

Il serait utile de voir ce système en action, mais les outils permettant de fournir 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 de nombreuses tentatives pour ajouter le 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 se terminer 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é.

Pada masa ini, hanya Google Chrome (yang menyediakan Profil Heap) yang melaksanakan alat pengurusan memori sebagai alat pembangunnya. Dalam artikel ini saya menggunakan Heap Profiler untuk menguji dan menunjukkan cara masa jalan JavaScript mengendalikan memori.

3. Menganalisis petikan timbunan

Sebelum mencipta kebocoran memori, lihat interaksi mudah yang mengumpul memori dengan sewajarnya. Mulakan dengan mencipta halaman HTML ringkas dengan dua butang, seperti yang ditunjukkan dalam Penyenaraian 1.

Senarai 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>

jQuery disertakan untuk memastikan sintaks mudah untuk mengurus pengikatan acara yang berfungsi merentas pelayar dan mematuhi amalan pembangunan yang paling biasa. Tambahkan tag skrip pada kelas pembocor dan kaedah JavaScript utama. Dalam persekitaran pembangunan, selalunya merupakan amalan yang lebih baik untuk menggabungkan fail JavaScript ke dalam satu fail. Untuk tujuan contoh ini, lebih mudah untuk meletakkan logik dalam fail berasingan.

Anda boleh menapis Heap Profiler untuk menunjukkan hanya kejadian kelas tertentu. Untuk memanfaatkan ciri ini, cipta kelas baharu yang merangkum gelagat objek bocor dan kelas ini mudah ditemui dalam Profiler Timbunan, seperti yang ditunjukkan dalam Penyenaraian 2.

Senarai 2. aset/skrip/leaker.js

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

 } 
};

Ikat butang Mula untuk memulakan objek Leaker dan berikannya kepada pembolehubah dalam ruang nama global. Anda juga perlu mengikat butang Musnah pada kaedah yang sepatutnya membersihkan objek Leaker dan menyediakannya untuk pengumpulan sampah, seperti yang ditunjukkan dalam Penyenaraian 3.

Senarai 3. aset/skrip/utama.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();

Kini anda sudah bersedia untuk mencipta objek, melihatnya dalam ingatan, dan kemudian melepaskannya.

1) Muatkan halaman indeks dalam Chrome. Oleh kerana anda memuatkan jQuery terus daripada Google, anda memerlukan sambungan Internet untuk menjalankan sampel ini.
2) Buka alat pembangun dengan membuka menu View dan memilih submenu Develop. Pilih arahan Alat Pembangun.
3) Pergi ke tab Profil dan dapatkan petikan timbunan, seperti yang ditunjukkan dalam Rajah 2.

Rajah 2. Tab Profil

4) Kembalikan perhatian anda kepada Web dan pilih Mula.
5). Dapatkan petikan timbunan yang lain.
6) Tapis syot kilat pertama dan cari contoh kelas Leaker, tetapi tiada kejadian boleh ditemui. Beralih kepada syot kilat kedua dan anda harus mencari contoh seperti yang ditunjukkan dalam Rajah 3.

Rajah 3. Contoh syot kilat

7) Kembalikan perhatian anda kepada Web dan pilih Musnah.
8). Dapatkan petikan timbunan ketiga.
9) Tapis syot kilat ketiga dan cari contoh kelas Leaker, tetapi tiada kejadian boleh ditemui. Semasa memuatkan syot kilat ketiga, anda juga boleh menukar mod analisis daripada Ringkasan kepada Perbandingan dan membandingkan syot kilat ketiga dan kedua. Anda melihat nilai offset sebanyak -1 (contoh objek Leaker dikeluarkan antara syot kilat).
panjang umur! Kutipan sampah adalah berkesan. Kini tiba masanya untuk memusnahkannya.

4 Kebocoran Memori 1: Penutupan

Cara mudah untuk mengelakkan objek daripada menjadi sampah dikumpul adalah dengan menetapkan selang waktu atau tamat masa untuk merujuk objek dalam panggilan balik. Untuk melihatnya dalam tindakan, kemas kini kelas leaker.js, seperti yang ditunjukkan dalam Penyenaraian 4.

Senarai 4. aset/skrip/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");
 }
};

Sekarang, apabila mengulangi langkah 1-9 dari bahagian sebelumnya, anda akan melihat dalam petikan ketiga bahawa objek Leaker berterusan dan selang itu terus berjalan selama-lamanya. Jadi apa yang berlaku? Sebarang pembolehubah tempatan yang dirujuk dalam penutupan dikekalkan oleh penutupan selagi penutupan itu wujud. Untuk memastikan bahawa panggilan balik kepada kaedah setInterval dilaksanakan apabila skop contoh Leaker diakses, pembolehubah ini perlu diberikan kepada pembolehubah tempatan sendiri, yang digunakan untuk mencetuskan onInterval dari dalam penutupan. Apabila onInterval menyala, ia boleh mengakses sebarang pembolehubah contoh dalam objek Leaker (termasuk dirinya sendiri). Walau bagaimanapun, selagi pendengar acara wujud, objek Leaker tidak akan dikumpul sampah.

Untuk menyelesaikan isu ini, cetuskan kaedah musnah yang ditambahkan pada objek pembocor sebelum mengosongkan rujukan yang disimpan kepadanya, dengan mengemas kini pengendali klik butang Musnah, seperti yang ditunjukkan dalam Penyenaraian 5.

Senarai 5. aset/skrip/utama.js

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

五、销毁对象和对象所有权

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

1、阻止它的引用计数下降到 0(例如,删除存在问题的事件侦听器和回调,并从任何服务取消注册)。
2、使用不必要的 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(){

 }  
};

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

  • 登录到索引页面。
  • 单击 Start。
  • 转到控制台并确认 Leaking 对象已被跟踪。
  • 单击 Destroy。
  • 回到控制台并键入 leak,以记录全局变量当前的内容。此刻该值应为空。
  • 获取另一个堆快照并过滤 Leaker 对象。您应留下一个 Leaker 对象。
  • 回到控制台并清除它。
  • 创建另一个堆配置文件。在清理控制台后,保留 leaker 的配置文件应已清除。

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

1)、在用户键入 JavaScript 时,在控制台中的一个交互式会话期间记录的对象。
2)、由 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
d48932f35e95ee8f303e1028865ed4402cacc6d41bbb37262a98f745aa00fbf0
更新 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. 由于保留引用导致的内存泄漏

从表面上看,它像一个不自然的示例,但它实际上非常常见。更加经典的面向对象框架中的事件侦听器常常遵循类似图 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 是一个诊断内存问题的宝贵工具,在开发时定期使用它也是一个不错的选择。在预测对象曲线图中要释放的具体资源时请设定具体的预期,然后进行验证。任何时候当您看到不想要的结果时,请仔细调查。

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

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