Rumah > Artikel > hujung hadapan web > Pemahaman mendalam tentang kebocoran memori dalam kemahiran JavaScript programs_javascript
Pengumpulan sampah membebaskan kita untuk menumpukan pada logik aplikasi (bukannya pengurusan memori). Walau bagaimanapun, kutipan sampah tidak ajaib. Memahami cara ia berfungsi, dan cara untuk mengekalkan memori yang sepatutnya telah dibebaskan sejak dahulu lagi, boleh membawa kepada aplikasi yang lebih pantas dan lebih dipercayai. Dalam artikel ini, pelajari pendekatan sistematik untuk mengesan kebocoran memori dalam aplikasi JavaScript, beberapa corak kebocoran biasa dan kaedah yang sesuai untuk menyelesaikan kebocoran ini.
1. Pengenalan
Apabila berurusan dengan bahasa skrip seperti JavaScript, adalah mudah untuk melupakan bahawa setiap objek, kelas, rentetan, nombor dan kaedah perlu memperuntukkan dan menyimpan memori. Butiran khusus peruntukan memori dan deallocation disembunyikan daripada bahasa dan pengumpul sampah masa jalan.
Banyak fungsi boleh dilaksanakan tanpa mengambil kira pengurusan memori, tetapi mengabaikannya boleh menyebabkan masalah besar dalam program. Objek yang tidak dibersihkan dengan betul mungkin kekal lebih lama daripada yang dijangkakan. Objek ini terus bertindak balas kepada peristiwa dan menggunakan sumber. Mereka memaksa penyemak imbas untuk memperuntukkan halaman memori daripada pemacu cakera maya, dengan ketara memperlahankan komputer (dan dalam kes yang melampau, menyebabkan penyemak imbas ranap).
Kebocoran memori ialah sebarang objek yang berterusan selepas anda tidak memiliki atau memerlukannya lagi. Dalam beberapa tahun kebelakangan ini, banyak penyemak imbas telah meningkatkan keupayaan mereka untuk menuntut semula memori daripada JavaScript semasa memuatkan halaman. Walau bagaimanapun, tidak semua penyemak imbas beroperasi dengan cara yang sama. Kedua-dua Firefox dan versi Internet Explorer yang lebih lama telah mengalami kebocoran memori yang berterusan sehingga penyemak imbas ditutup.
Banyak corak klasik yang menyebabkan kebocoran memori pada masa lalu tidak lagi menyebabkan kebocoran memori dalam pelayar moden. Walau bagaimanapun, terdapat trend berbeza yang mempengaruhi kebocoran memori hari ini. Ramai yang mereka bentuk aplikasi web untuk dijalankan dalam satu halaman tanpa menyegarkan halaman keras. Dalam satu halaman seperti itu, mudah untuk mengekalkan memori yang tidak lagi diperlukan atau relevan apabila berpindah dari satu keadaan aplikasi ke keadaan lain.
Dalam artikel ini, pelajari tentang kitaran hayat asas objek, cara pengumpulan sampah menentukan sama ada objek telah dibebaskan dan cara menilai kemungkinan tingkah laku bocor. Selain itu, ketahui cara menggunakan Heap Profiler dalam Google Chrome untuk mendiagnosis isu memori. Beberapa contoh menunjukkan cara menyelesaikan kebocoran memori daripada penutupan, log konsol dan gelung.
2. Kitaran hayat objek
Untuk memahami cara mengelakkan kebocoran memori, anda perlu memahami kitaran hayat asas objek. Apabila objek dicipta, JavaScript secara automatik memperuntukkan memori yang sesuai untuk objek tersebut. Mulai saat ini, pemungut sampah terus menilai objek untuk melihat sama ada ia masih objek yang sah.
Pengumpul sampah mengimbas objek secara berkala dan mengira bilangan objek lain yang mempunyai rujukan kepada setiap objek. Jika objek mempunyai 0 rujukan (tiada objek lain merujuk kepadanya), atau satu-satunya rujukan kepada objek adalah bulat, maka memori objek boleh dituntut semula. Rajah 1 menunjukkan contoh pemungut sampah yang menuntut semula ingatan.
Rajah 1. Menuntut semula ingatan melalui kutipan sampah
Melihat sistem ini berfungsi, tetapi alat untuk menyediakan fungsi ini adalah terhad. Satu cara untuk mengetahui jumlah memori yang digunakan oleh aplikasi JavaScript anda adalah dengan menggunakan alatan sistem untuk melihat peruntukan memori penyemak imbas anda. Terdapat beberapa alatan yang boleh memberikan anda penggunaan semasa dan graf arah aliran penggunaan memori proses dari semasa ke semasa.
Sebagai contoh, jika anda telah memasang XCode pada Mac OSX, anda boleh melancarkan apl Instrumen dan melampirkan alat Monitor Aktivitinya pada penyemak imbas anda untuk analisis masa nyata. Pada Windows®, anda boleh menggunakan Pengurus Tugas. Anda tahu anda mengalami kebocoran memori jika anda mendapati peningkatan berterusan dalam penggunaan memori dari semasa ke semasa semasa anda menggunakan aplikasi anda.
Memerhati jejak memori penyemak imbas hanya memberikan petunjuk yang sangat kasar tentang penggunaan memori sebenar aplikasi JavaScript. Data penyemak imbas tidak memberitahu anda objek yang dibocorkan dan tiada jaminan bahawa data itu benar-benar sepadan dengan jejak memori sebenar aplikasi anda. Selain itu, disebabkan isu pelaksanaan dalam sesetengah penyemak imbas, elemen DOM (atau objek peringkat aplikasi alternatif) mungkin tidak dikeluarkan apabila elemen yang sepadan dimusnahkan dalam halaman. Ini adalah benar terutamanya untuk teg video, yang memerlukan penyemak imbas untuk melaksanakan infrastruktur yang lebih terperinci.
Terdapat banyak percubaan untuk menambah penjejakan peruntukan memori dalam perpustakaan JavaScript sisi klien. Malangnya, tiada satu pun percubaan yang boleh dipercayai. Contohnya, pakej stats.js yang popular tidak disokong kerana ketidaktepatan. Secara umum, cuba mengekalkan atau menentukan maklumat ini daripada pelanggan adalah bermasalah kerana ia memasukkan overhed ke dalam aplikasi dan tidak boleh ditamatkan dengan pasti.
Penyelesaian yang ideal ialah vendor penyemak imbas menyediakan satu set alat dalam penyemak imbas yang membantu anda memantau penggunaan memori, mengenal pasti objek yang bocor dan menentukan sebab objek tertentu masih ditandakan sebagai simpanan.
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.
3. Analyse des instantanés de tas
Avant de créer une fuite de mémoire, examinez une interaction simple qui collecte la mémoire de manière appropriée. Commencez par créer une simple page HTML avec deux boutons, comme indiqué dans le listing 1.
Liste 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 est inclus pour garantir une syntaxe simple de gestion des liaisons d'événements qui fonctionne sur tous les navigateurs et adhère 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.
Liste 2. actifs/scripts/leaker.js
var Leaker = function(){}; Leaker.prototype = { init:function(){ } };
Liez le bouton Démarrer pour initialiser l'objet Leaker et l'attribuer à 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.
Liste 3. actifs/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, à le visualiser en mémoire, puis à le libérer.
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 obtenez un instantané du tas, comme illustré dans la figure 2.
Figure 2. Onglet Profils
4) Ramenez votre attention sur le Web et sélectionnez Démarrer.
5). Obtenez un autre instantané de tas.
6) Filtrez le premier instantané et recherchez les instances de la classe Leaker, mais aucune instance ne peut être trouvée. Passez au deuxième instantané et vous devriez trouver une instance comme le montre la figure 3.
Figure 3. Exemple d'instantané
7) Ramenez votre attention sur le Web et sélectionnez Détruire.
8). Obtenez le troisième instantané de tas.
9) Filtrez le troisième instantané et recherchez les instances de la classe Leaker, mais aucune instance ne peut être 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 voyez une valeur de décalage de -1 (une instance de l'objet Leaker a été publiée entre les instantanés).
Longue vie! La collecte des déchets est efficace. Il est maintenant temps de le détruire.
4. Fuite de mémoire 1 : Fermeture
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.
Liste 4. actifs/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, 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 tant 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 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énements existe, l'objet Leaker ne sera pas récupéré.
Pour résoudre ce problème, déclenchez la méthode destroy ajoutée à l'objet fuyard avant d'effacer la référence stockée à celui-ci, en mettant à jour le gestionnaire de clic du bouton Destroy, comme indiqué dans le listing 5.
Liste 5. actifs/scripts/main.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(){ } };
可采取以下步骤来演示控制台的影响。
控制台日志记录对总体内存配置文件的影响可能是许多开发人员都未想到的极其重大的问题。记录错误的对象可以将大量数据保留在内存中。注意,这也适用于:
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 是一个诊断内存问题的宝贵工具,在开发时定期使用它也是一个不错的选择。在预测对象曲线图中要释放的具体资源时请设定具体的预期,然后进行验证。任何时候当您看到不想要的结果时,请仔细调查。
在创建对象时要计划该对象的清理工作,这比在以后将一个清理阶段移植到应用程序中要容易得多。常常要计划删除事件侦听器,并停止您创建的间隔。如果认识到了您应用程序中的内存使用,您将得到更可靠且性能更高的应用程序。