写在前面
像C语言这种底层语言,一般都有底层的内存管理接口,比如malloc()和free()。相反,JavaScript是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放。 释放的过程称为垃圾回收。这个“自动”是混乱的根源,并让JavaScript(和其他高级语言)开发者错误的感觉他们可以不关心内存管理。
内存管理
内存的生命周期
不管是什么语言,内存的生命周期基本是一致的:
- 分配所需要的内存
- 使用分配到的内存(读,写)
- 不需要时将其释放/归还
内存分配
在定义变量时,就完成了内存分配。
var n = 123; // 给数值变量分配内存
var s = "azerty"; // 给字符串分配内存
var o = {
a: 1,
b: null
}; // 给对象及其包含的值分配内存
使用内存
这个过程实际上就是对分配的内存进行读取和写入的操作。
当内存不再需要时释放
大部分内存管理问题出现在这里,原因是难以找到哪些被分配的内存确实不再需要了,需要开发人员手动来释放。
JavaScript嵌入了‘垃圾回收器’,主要目的是跟踪内存的分配和使用,以便当分配的内存不再需要时,自动释放。
但这并不能完全解决上述的问题,因为要知道是否仍然需要这块内存是无法判定的。
垃圾回收
如上所述自动寻找是否一些内存“不再需要”的问题是无法判定的。因此,垃圾回收实现只能有限制的解决一般问题。
引用
垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。例如,一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。
在这里,“对象”的概念不仅特指 JavaScript 对象,还包括函数作用域(或者全局词法作用域)。
分类
现在垃圾回收机制一般有两种方式:
- 引用计数
- 标记-清除
引用计数
这是最初级的算法,此算法把‘对象是否不再需要’简化为‘对象有没有被其他对象引用’。 如果没有引用该对象(零引用),对象将被回收。
var o = {
a: {
b:2
}
};
// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量o
// 很显然,没有一个可以被垃圾收集
var o2 = o; // o2变量是第二个对“这个对象”的引用
o = 1; // 现在,“这个对象”的原始引用o被o2替换了
var oa = o2.a; // 引用“这个对象”的a属性
// 现在,“这个对象”有两个引用了,一个是o2,一个是oa
o2 = "yo"; // 最初的对象现在已经是零引用了
// 他可以被垃圾回收了
// 然而它的属性a的对象还在被oa引用,所以还不能回收
oa = null; // a属性的那个对象现在也是零引用了
// 它可以被垃圾回收了
限制
此算法有个限制:无法处理循环引用的事例。
function f(){
var o = {};
var o2 = {};
o.a = o2; // o 引用 o2
o2.a = o; // o2 引用 o
return "azerty";
}
f();
此事例中,两个对象被创建,并互相引用,形成了一个循环。它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。
IE 6, 7 使用引用计数方式对 DOM 对象进行垃圾回收。该方式常常造成对象被循环引用时内存发生泄漏。
var div;
window.onload = function(){
div = document.getElementById("myDivElement");
div.circularReference = div;
div.lotsOfData = new Array(10000).join("*");
};
myDivElement 这个 DOM 元素里的 circularReference 属性引用了 myDivElement,造成了循环引用。如果该属性没有显示移除或者设为 null,引用计数式垃圾收集器将总是且至少有一个引用,并将一直保持在内存里的 DOM 元素,即使其从DOM 树中删去了。如果这个 DOM 元素拥有大量的数据 (如上的 lotsOfData 属性),而这个数据占用的内存将永远不会被释放。
标记-清除
这个算法把‘对象是否不再需要’定义为 ‘对象是否可以获得’, 这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。
从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法本身和它对“对象是否不再需要”的简化定义。
循环引用不再是问题
在上面的示例中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,他们将会被垃圾回收器回收。第二个示例同样,一旦 div 和其事件处理无法从根获取到,他们将会被垃圾回收器回收。
但依然有个小小限制,即那些无法从根对象查询到的对象将会被清除。 但在实践中,很少会碰到类似情况,这也是我们不太关心垃圾回收机制的原因。
总结
内存的生命周期分为三个部分,我们在定义变量时,占用了内存,在经历了相关操作后,变量不再需要时,将相关对象回收,释放内存。那如何回收呢,提供了两种方法,引用计数和标记清除,引用计数有个比较大的限制,那就是无法回收循环引用的对象,而标记清除方法不存在这个问题,虽然依然有个小小限制,但无伤大雅,二者区别在于算法定义的不同,以及基于此的算法方式不同。由此可见,垃圾回收并不能完全将不再需要的变量回收,这只是个优化内存的方式。