导言
像 C 语言这样的低级语言提供了手动内存管理的原语malloc()
和free()
。而 JavaScript 则在对象创建时自动分配内存并在它们不再使用时释放它们(垃圾回收)。这种自动化也是一种困惑的来源,那就是开发人员会错误地认为他们不需要考虑内存管理了。
内存的生命周期
无论什么编程语言,内存的生命周期总是相似的:
1.分配你需要的内存。
2.使用分配的内存(读,写)。
3.当你不再使用时,释放已经分配的内存。
在 JavaScript 中的分配
值初始化
JavaScript 在变量声明的时候就替程序员做好了内存分配的活了。
var n = 123; // allocates memory for a number
var s = 'azerty'; // allocates memory for a string
var o = {
a: 1,
b: null
}; // allocates memory for an object and contained values
// (like object) allocates memory for the array and
// contained values
var a = [1, null, 'abra'];
function f(a) {
return a + 2;
} // allocates a function (which is a callable object)
// function expressions also allocate an object
someElement.addEventListener('click', function() {
someElement.style.backgroundColor = 'blue';
}, false);
通过函数调用分配内存
var d = new Date(); // allocates a Date object
var e = document.createElement('div'); // allocates a DOM element
var s = 'azerty';
var s2 = s.substr(0, 3); // s2 is a new string
// Since strings are immutable value,
// JavaScript may decide to not allocate memory,
// but just store the [0, 3] range.
var a = ['ouais ouais', 'nan nan'];
var a2 = ['generation', 'nan nan'];
var a3 = a.concat(a2);
// new array with 4 elements being
// the concatenation of a and a2 elements
使用值
通过读和写已经分配的内存来使用值。这可以通过读或写一个变量的值或一个对象属性,或者传递一个参数给函数来实现。
在内存不再使用时释放
大多数的内存管理问题发生在这个阶段。最难得任务是确定什么时候已分配的内存不再需要了。通常需要程序员来决定程序中的那一部分不再需要,并释放它。
高级语言会嵌入一小段垃圾回收的代码,用于追踪内存的分配和使用,以确定这块已分配的内存在什么时候不再需要,并自动释放它。这个过程是一个近似的过程,因为判断某一段已分配的内存是否需要是不可预测的(不能被算法决定)。
垃圾回收
垃圾回收算法就是为了解决已分配内存是否还需要的问题的。这一节将会介绍主要的垃圾回收算法和它们的实现。
引用
垃圾回收算法主要依赖的概念是“引用”。在内存管理的上下文中,当一个对象访问了另一个对象(无论是显式还是隐式)可以称为一个引用。比如,一个 JavaScript 对象引用它的原型(隐式引用)还有它的属性值(显式引用)。
引用计数的垃圾回收
这是最朴素的垃圾回收算法。这个算法将“不再使用的对象”归纳为“没有其它对象引用的对象”。当没有指向一个对象的引用时,就可以考虑回收它了。
var o = {
a: {
b: 2
}
};
// 2 objects are created. One is referenced by the other as one of its properties.
// The other is referenced by virtue of being assigned to the 'o' variable.
// Obviously, none can be garbage-collected
var o2 = o; // the 'o2' variable is the second thing that
// has a reference to the object
o = 1; // now, the object that was originally in 'o' has a unique reference
// embodied by the 'o2' variable
var oa = o2.a; // reference to 'a' property of the object.
// This object now has 2 references: one as a property,
// the other as the 'oa' variable
o2 = 'yo'; // The object that was originally in 'o' has now zero
// references to it. It can be garbage-collected.
// However its 'a' property is still referenced by
// the 'oa' variable, so it cannot be freed
oa = null; // The 'a' property of the object originally in o
// has zero references to it. It can be garbage collected.
局限:循环引用
引用计数的方法的局限是循环引用。在下面的例子中,两个对象在被创建时互相引用对方,形成了一个闭环。它们将在函数调用后脱离作用域,所以它们已经没用了并且可以释放了。然而,引用计数的方法会考虑这两个对象至少有一个引用,并决定不回收它们。
function f() {
var o = {};
var o2 = {};
o.a = o2; // o references o2
o2.a = o; // o2 references o
return 'azerty';
}
f();
真实示例
Internet Explorer 6和7使用引用计数的垃圾回收器来回收 DOM 对象。闭环是一个很普遍的产生内存泄露的问题。
var div;
window.onload = function() {
div = document.getElementById('myDivElement');
div.circularReference = div;
div.lotsOfData = new Array(10000).join('*');
};
如上所示,如果 DOM 元素包含大量数据,被这些数据消耗的内存将不会被释放。
标记-擦除算法
这种算法将“一个不再使用的对象”归纳为“一个不可达的对象”。
算法先假定一系列对象为根对象(在 JavaScript 中,根对象是全局对象)。垃圾回收器会周期性地从这些根对象开始,找到所有根对象引用的对象,然后再找这些对象引用的对象,直到找到所有被引用的对象。垃圾回收器会找到所有可达对象并收集所有不可达对象。
直到2012年,所有的现代浏览器都使用的是标记-擦除算法。所有在这个领域的优化也是基于该算法的,但不变的是垃圾回收的目的,“回收掉那些不再使用的对象”。
循环引用不再是一个问题
在上面的例子中,函数调用返回后,那两个循环引用的对象将不会被任何可达树上的对象引用,即不可达。因此他们会被垃圾回收器回收。
局限:对象必须显式不可达
即使是一种局限,但在实际的生产活动中很难遇到,所以人们也不太在意垃圾回收的这点局限。