はじめに
プログラムの実行にはメモリが必要です。プログラムがメモリを要求するたびに、オペレーティング システムまたはランタイムはメモリを提供する必要があります。いわゆるメモリ リークとは、単に使用されなくなり、期限内に解放されなかったメモリのことです。メモリ リークをより適切に回避するために、まず Javascript ガベージ コレクション メカニズムを導入します。
C や C などの言語では、開発者はアプリケーションとメモリのリサイクルを直接制御できます。ただし、Java、C#、および JavaScript 言語では、変数のメモリ空間の適用と解放はプログラム自体によって処理されるため、開発者は気にする必要はありません。つまり、JavaScript には自動ガベージコレクション機構 (Garbage Collection) が備わっています。
1. ガベージ コレクションの必要性
以下の文章は『JavaScript 入門 (第 4 版)』より引用です
string オブジェクトと配列には固定サイズがないため、サイズがわかっている場合は動的に割り当てることができます。 JavaScript プログラムが文字列、配列、またはオブジェクトを作成するたびに、インタープリターはそのエンティティを格納するためにメモリを割り当てる必要があります。このようにメモリが動的に割り当てられる場合は、再利用できるように最終的にメモリを解放する必要があります。そうしないと、JavaScript インタプリタがシステム内の利用可能なメモリをすべて消費し、システムがクラッシュする原因になります。
この文章では、システムにガベージ コレクションが必要な理由を説明します。JavaScript は C/C とは異なり、独自のガベージ コレクション メカニズムを備えています。
JavaScript のガベージ コレクションのメカニズムは、使用されなくなった変数を見つけて、その変数が占有しているメモリを解放するという非常に単純な仕組みですが、このプロセスはオーバーヘッドが比較的大きいため、それほど時間はかかりません。ガベージ コレクターは、一定の間隔で定期的に実行されます。
var a = "浪里行舟"; var b = "前端工匠"; var a = b; //重写a
このコードを実行すると、文字列 "Wanglizhou" はその参照を失います (以前は a によって参照されていました)。システムはこの事実を検出した後、文字列の記憶領域を解放します。再利用できます。
2. ガベージ コレクション メカニズム
ガベージ コレクション メカニズムは、どのメモリが不要になったかをどのようにして知るのでしょうか?
ガベージ コレクションには、マークのクリアと参照カウントの 2 つの方法があります。参照カウントはあまり一般的には使用されず、マークアンドスイープがより一般的に使用されます。
1. マークのクリア
これは、JavaScript で最も一般的に使用されるガベージ コレクション メソッドです。変数が実行環境に入るとき、その変数を「環境に入る」としてマークします。論理的に言えば、環境に入る変数によって占有されているメモリは、実行フローが対応する環境に入る限り使用される可能性があるため、解放されることはありません。変数が環境から離れると、「環境から離れる」とマークされます。
ガベージ コレクターは、実行時にメモリに格納されているすべての変数にマークを付けます。次に、環境内の変数と、環境内の変数によって参照されるタグを削除します。これ以降にマークされた変数は、環境内の変数がこれらの変数にアクセスできなくなるため、削除される変数とみなされます。やっと。ガベージ コレクターはメモリ クリーニング作業を完了し、マークされた値を破棄し、それらが占有しているメモリ領域を再利用します。
例を使用してこの方法を説明しましょう:
var m = 0,n = 19 // 把 m,n,add() 标记为进入环境。 add(m, n) // 把 a, b, c标记为进入环境。 console.log(n) // a,b,c标记为离开环境,等待垃圾回收。 function add(a, b) { a++ var c = a + b return c }
2. 参照カウント
つまり、 「参照カウント」とは、言語エンジンがすべてのリソース (通常はさまざまな値) への参照数をメモリに保存する「参照テーブル」を持っていることを意味します。値の参照数が 0 の場合は、その値が使用されなくなったことを意味するため、メモリを解放できます。
上の図では、左下隅の 2 つの値には参照がないため、解放できます。
値が不要になったが参照番号が 0 でない場合、ガベージ コレクション メカニズムはこのメモリを解放できず、メモリ リークが発生します。
var arr = [1, 2, 3, 4]; arr = [2, 4, 5] console.log('浪里行舟');
上記のコードでは、配列 [1, 2, 3, 4] は値であり、メモリを占有します。変数 arr はこの値への唯一の参照であるため、参照の数は 1 です。次のコードでは arr は使用されていませんが、引き続きメモリを占有します。メモリを解放する方法については、以下で紹介します。
コードの 3 行目では、配列 [1, 2, 3, 4] によって参照される変数 arr が別の値を取得し、配列 [1, 2, 3, 4] への参照の数が減ります。 1 ずつ増加すると、この時点でその参照カウントは 0 になります。これは、この値にアクセスする方法がなくなったことを意味するため、この値が占有しているメモリ空間を回復できます。
しかし、参照カウントの最大の問題は、循環参照です。
function func() { let obj1 = {}; let obj2 = {}; obj1.a = obj2; // obj1 引用 obj2 obj2.a = obj1; // obj2 引用 obj1 }
関数 func が実行されると、戻り値は未定義であるため、関数全体と内部変数はリサイクルされる必要がありますが、参照カウント方式では、obj1 と obj2 の参照数は 0 ではないため、再利用されません。
循環参照の問題を解決するには、使用しない場合は手動で空に設定するのが最善です。上記の例は次のように実行できます:
obj1 = null; obj2 = null;
3. メモリ リークはどのような状況で発生する可能性がありますか?
虽然JavaScript会自动垃圾收集,但是如果我们的代码写法不当,会让变量一直处于“进入环境”的状态,无法被回收。下面列一下内存泄漏常见的几种情况:
1. 意外的全局变量
function foo(arg) { bar = "this is a hidden global variable"; }
bar没被声明,会变成一个全局变量,在页面关闭之前不会被释放。
另一种意外的全局变量可能由 this 创建:
function foo() { this.variable = "potential accidental global"; } // foo 调用自己,this 指向了全局对象(window) foo();
在 JavaScript 文件头部加上 'use strict',可以避免此类错误发生。启用严格模式解析 JavaScript ,避免意外的全局变量。
2. 被遗忘的计时器或回调函数
var someResource = getData(); setInterval(function() { var node = document.getElementById('Node'); if(node) { // 处理 node 和 someResource node.innerHTML = JSON.stringify(someResource)); } }, 1000);
这样的代码很常见,如果id为Node的元素从DOM中移除,该定时器仍会存在,同时,因为回调函数中包含对someResource的引用,定时器外面的someResource也不会被释放。
3. 闭包
function bindEvent(){ var obj=document.createElement('xxx') obj.onclick=function(){ // Even if it is a empty function } }
闭包可以维持函数内局部变量,使其得不到释放。上例定义事件回调时,由于是函数内定义函数,并且内部函数--事件回调引用外部函数,形成了闭包。
// 将事件处理函数定义在外面 function bindEvent() { var obj = document.createElement('xxx') obj.onclick = onclickHandler } // 或者在定义事件处理函数的外部函数中,删除对dom的引用 function bindEvent() { var obj = document.createElement('xxx') obj.onclick = function() { // Even if it is a empty function } obj = null }
解决之道,将事件处理函数定义在外部,解除闭包,或者在定义事件处理函数的外部函数中,删除对dom的引用。
4. 没有清理的DOM元素引用
有时,保存 DOM 节点内部数据结构很有用。假如你想快速更新表格的几行内容,把每一行 DOM 存成字典(JSON 键值对)或者数组很有意义。此时,同样的 DOM 元素存在两个引用:一个在 DOM 树中,另一个在字典中。将来你决定删除这些行时,需要把两个引用都清除。
var elements = { button: document.getElementById('button'), image: document.getElementById('image'), text: document.getElementById('text') }; function doStuff() { image.src = 'http://some.url/image'; button.click(); console.log(text.innerHTML); } function removeButton() { document.body.removeChild(document.getElementById('button')); // 此时,仍旧存在一个全局的 #button 的引用 // elements 字典。button 元素仍旧在内存中,不能被 GC 回收。 }
虽然我们用removeChild移除了button,但是还在elements对象里保存着#button的引用,换言之,DOM元素还在内存里面。
四、内存泄漏的识别方法
新版本的chrome在 performance 中查看:
步骤:
● 打开开发者工具 Performance
● 勾选 Screenshots 和 memory
● 左上角小圆点开始录制(record)
● 停止录制
图中 Heap 对应的部分就可以看到内存在周期性的回落也可以看到垃圾回收的周期,如果垃圾回收之后的最低值(我们称为min),min在不断上涨,那么肯定是有较为严重的内存泄漏问题。
避免内存泄漏的一些方式:
减少不必要的全局变量,或者生命周期较长的对象,及时对无用的数据进行垃圾回收
注意程序逻辑,避免“死循环”之类的
避免创建过多的对象
总而言之需要遵循一条原则:不用了的东西要及时归还
五、垃圾回收的使用场景优化
1. 数组array优化
将[]赋值给一个数组对象,是清空数组的捷径(例如: arr = [];),但是需要注意的是,这种方式又创建了一个新的空对象,并且将原来的数组对象变成了一小片内存垃圾!实际上,将数组长度赋值为0(arr.length = 0)也能达到清空数组的目的,并且同时能实现数组重用,减少内存垃圾的产生。
const arr = [1, 2, 3, 4]; console.log('浪里行舟'); arr.length = 0 // 可以直接让数字清空,而且数组类型不变。 // arr = []; 虽然让a变量成一个空数组,但是在堆上重新申请了一个空数组对象。
2. 对象尽量复用
对象尽量复用,尤其是在循环等地方出现创建新对象,能复用就复用。不用的对象,尽可能设置为null,尽快被垃圾回收掉。
var t = {} // 每次循环都会创建一个新对象。 for (var i = 0; i < 10; i++) { // var t = {};// 每次循环都会创建一个新对象。 t.age = 19 t.name = '123' t.index = i console.log(t) } t = null //对象如果已经不用了,那就立即设置为null;等待垃圾回收。
3. 在循环中的函数表达式,能复用最好放到循环外面。
// 在循环中最好也别使用函数表达式。 for (var k = 0; k < 10; k++) { var t = function(a) { // 创建了10次 函数对象。 console.log(a) } t(k) } // 推荐用法 function t(a) { console.log(a) } for (var k = 0; k < 10; k++) { t(k) } t = null
以上がJavaScript でのガベージ コレクションとメモリ リークの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。