你可能听说过JAVA、.NET、PHP这些语言有垃圾回收的内存管理机制,但是很少会听到JavaScript也有自己的内存管理机制,JavaScript同样有着类似的垃圾回收功能。本文主要讲述了JavaScript的垃圾回收原理和具体的过程。
简介
在底层语言中,比如C,有专门的内存管理机制,比如malloc() 和 free()。而Javascript是有垃圾回收(garbage collection)机制的,也就是说JS解释器会自动分配和回收内存。这样就有人觉得,我用的是高级语言,就不用关心内存管理了,其实这是不对的。
内存的生命周期
尽管语言不尽相同,而每种语言中内存的生命周期都是相似的:
1.当需要的时候分配内存
2.对内存进行读写操作
3.当上面分配的内存不再需要的时候,将他们释放掉
对于1,2两步,几乎所有语言操作起来都是明确地或者说很直观,没什么好说的。而在像Javascript一样的高级语言中,第三步操作就显得不那么直观。
Javascript中分配内存空间
变量初始化
当变量初始化的时候,Javascript会自动分配相应的内存空间(注:这里MDN上关于这里用的是Value initialization,到底是声明,还是在赋值时候分配空间,还要再学习一下)
var n = 123; // 为数字分配空间
var s = “azerty”; // 字符串
var o = {
a: 1,
b: null
}; // 为对象和它包含的属性分配内存空间
var a = [1, null, "abra"]; // (类似对象)给数组和它里面的元素分配空间
function f(a){
return a + 2;
} // 为函数分配空间
// 函数有时也会为分配对象空间
someElement.addEventListener(‘click', function(){
someElement.style.backgroundColor = ‘blue'; //个人补充,未考证,这里会为someElement分配空间,如注释所说,为对象分配空间
}, false);
函数调用时候分配空间
有的函数调用,会产生上面说的那种 为对象分配空间
var d = new Date();
var e = document.createElement('div'); // allocates an DOM element还有下面这种
var s = “azerty”;
var s2 = s.substr(0, 3); // s2 is a new string
// 由于Javascript中字符串是不可变的,所以Javascript也许并没有为s2中的字符串分配新空间,而是只存了[0, 3]的区间(用来索引)
var a = ["ouais ouais", "nan nan"];
var a2 = ["generation", "nan nan"];
var a3 = a.concat(a2); // 新的空间来存储数组a3
操作变量值
没什么好说的,读、写、函数调用。
内存不再被使用时,将它们释放掉
许多内存管理机制的问题都出现在这里。最麻烦的问题是确认“这块内存空间已经不需要了”。这往往需要程序员告知,这个程序中,这块内存已经不需要了,你们回收吧。
而高级语言解释器中嵌入了一个叫做“垃圾回收(garbage collector)”的工具,用来跟踪内存分配和使用情况,以便在它们不需要的时候将其自动回收。然而有个问题,一块内存空间是不是还有用,是具有不确定性的,也就是说,这个是没法用算法精确算出来的。
垃圾回收
如上所述原因,垃圾回收机制采取了一种有限的解决方案来处理上面的不确定性问题。下面介绍集中垃圾回收算法的思想以及相应的局限:
引用
这种方法,用到了一种引用的思想。当a能访问A时,就说A引用了a(不论是直接还是间接的)。比如,一个Javascript对象会引用他的原型(间接引用)和它的各个属性(直接引用)。
这种情形下,对象就被扩展的更广义了,在原生对象的基础上,还包含了函数的作用域链(或者全局的词法作用域)。
引用计数
这种方法是最拿衣服(naive)的垃圾回收算法。它把“可以回收”的标准定义为“没有其他人引用这个对象”(原文:This algorithm reduces the definition of “an object is not needed anymore” to “an object has no other object referencing to it”)。也就是说,只有当对象没有被引用的时候,才会被当作垃圾回收掉。
举个例子
var o = { // 称之为外层对象
a: { //称之为内层对象
b:2
}
}; // 创建了两个对象 内层对象作为外层对象的属性而被引用
// 而外层对象被变量o引用
// 显然,没有人会被垃圾回收
var o2 = o; // o2 は上記の外側のオブジェクトも参照します。幸いなことに、外側のオブジェクトの参照カウントは「2」です (o と o2 によって参照されます)
o = 1; // これで、o は外側のオブジェクトを参照せず、o2 のみが参照し、参照カウントは次のようになります。 '1'
var oa = o2.a; // oa は内部オブジェクトを参照します
// 内部オブジェクトは外部オブジェクトのプロパティとして、および oa の両方で参照され、参照カウントは '2'
o2 = "yo"; // さて、o2 は外側のオブジェクトを参照しなくなり、外側のオブジェクトの参照カウントは "0" になります
// これは、外側のオブジェクトを「ガベージ コレクション」できることを意味します
// ただし、内部オブジェクトは依然として oa によって参照されているため、リサイクルされていません (個人的なメモ: ここにクロージャのヒントがあります)
oa = null; // oa は内部オブジェクトを参照しません
// 内部オブジェクトもガベージ コレクションされます
制限事項: 循環参照
以下のコードを見てください:
function f(){
var o = {};
var o2 = {};
o.a = o2; // o を参照します
o2.a = o;引用
「azerty」を返す;
}
f();
// o o2 2 つのオブジェクトは循環参照を形成します
// 関数が実行されると、それらは f のスコープ内でロックされ、外部の誰もそれらを使用できなくなります
// したがって、それらには既存の値がなく、ガベージ コレクションを行ってメモリを解放する必要があるのは当然です
// ただし、それらの参照カウントは "0" ではありません
// したがって、この参照カウント メカニズムの下では、 、リサイクルされません
実際の例
ブラウザの IE6 および 7 バージョンでは、参照カウント メカニズムが使用されます。したがって、次のコードは IE6 および 7 で確実にメモリ リークを引き起こす可能性があります
var div = document.createElement("div");
div.onclick = function(){
doSomething();
} // div の onclick 属性は function// ただし、div はハンドラーのスコープ内にあるため、この関数はこの div を参照します。
// 上記の循環参照が発生し、メモリ リークが発生します。アルゴリズムをマークしてクリア
このアルゴリズムは「ルート」を定義し、定期的に「ルート」から開始して「ルート」の下にあるすべてのオブジェクトを検索し、「ルート」からこのオブジェクトを参照するパスを見つけられるかどうかを確認します。ガベージ コレクション プログラムは、さまざまな「ルート」から開始して、すべてのオブジェクトが「到達不能」であるかどうかを区別でき、オブジェクトが「到達不能」の場合はリサイクルされます。
このアルゴリズムは、参照カウント アルゴリズムよりも優れています。なぜなら、「オブジェクトの参照カウントが 0 である」ということは、「このオブジェクトは到達不能である」と推測でき、その逆の命題は偽であるからです。言い換えれば、このアルゴリズムはガベージ コレクションの範囲を拡張します。
循環参照はもはや問題ではありません
上記の循環参照の例では、関数が戻ると、o と o2 はどちらも参照されなくなります。つまり、それらは「到達不能」になります。 、自然とゴミに回収されていきました。
制限事項: オブジェクトは明示的に「到達不能」である必要があります。
制限事項ではありますが、この状況は実際にはめったに起こらないため、これに注意を払う人はほとんどいません。