从面向对象看JavaScript(四)—— 内存管理

前言

  与java类似,JavaScript也有两种垃圾回收方式,分别是标记清除和引用计数。目前基本上都是使用标记清除的方式,引用计数存在循环引用的问题,实际中很少使用,但IE中的BOM和DOM对象却是用引用计数方式来做垃圾回收,非常容易造成内存泄漏。

标记清除

  垃圾回收器会给所有存储在内存中的变量都打上标记,然后去掉所有在执行环境中的变量,以及被环境中的变量所引用的变量的标记。剩下还有标记的变量就是垃圾回收器将会回收的变量。

  对比java中的标记清除策略,java会从堆栈和静态存储区开始,遍历所有引用,对每一个引用,再追踪它引用的每个对象,然后对每个对象再追踪它引用的下一层对象。如此层层深入,最终能遍历所有“活”的对象,其余对象则在下一次垃圾回收器工作时被回收。

  不难看出,这两种语言的标记清除策略是一回事。JavaScript执行环境中的变量就相当于java堆栈和静态存储区的变量。

  如果采用这种回收策略,我们基本不用担心内存泄漏的问题。因为只要函数作用域结束了,自然会退出执行环境。只要变量不在执行环境中,即便存在循环引用,也会被垃圾回收器回收。只有全局环境下的变量和函数在程序运行的整个周期都不会被回收,如果不用的话,需要手动设为null。另外,闭包由于会保留其上级函数的变量对象,使得垃圾回收器无法清除相关的变量和函数,因此会占用较多的内存,所以使用闭包时要注意。

引用计数

  对内存中的每一个对象都记录其引用数,只要其引用数为0,则会被垃圾回收。但是如果存在循环引用,则会造成内存泄漏:

1
2
3
4
5
6
function problem(){
var objectA = new Object();
var objectB = new Object();
objectA.someOtherObject = objectB;
objectB.anotherObject = objectA;
}

  我们记problem函数里定义的两个对象为对象A和对象B。如果调用problem函数,则在函数结束前,对象A和对象B的引用数均为2。在执行结束后,objectA和objectB这两个引用由于存在堆栈里,会马上被释放。但对象A仍然被对象B的anotherObject属性引用;对象B也仍然被对象A的someOtherObject属性引用。所以这两个对象将永远不会被释放。

  如果大量调用problem函数,那么将会导致大量内存不能被回收。

  值得开心的事,这种引用计数方式已经基本不被采用。但IE的BOM和DOM对象仍然是用这种策略来进行垃圾回收的。因此使用BOM和DOM对象时要加倍小心,防止循环引用的发生。下面看一个例子:

1
2
3
4
var element = document.getElementById("some_element");
var myObject = new Object();
myObject.element = element;
element.someObject = myObject;

  element是DOM元素,它和myObject存在循环引用问题。在使用完毕DOM或BOM对象后,要记住将它们的引用设为null。

闭包的循环引用问题

  如果同时使用闭包和DOM或BOM对象,那么更加要留心循环引用的问题了。

1
2
3
4
5
6
function assignHandler(){
var element = document.getElementById("someElement");
element.onclick = function(){
alert(element.id);
};
}

  元素的点击事件函数的内存属性[[scope]]指向的作用域链里保存了assignHandler的活动对象,该活动对象里存在element的引用。同时。element的onclick属性也引用了点击事件函数。不难看出,闭包的循环引用往往是由其作用域链产生的。

  我们可以这样修改代码:

1
2
3
4
5
6
7
8
function assignHandler(){
var element = document.getElementById("someElement");
var id = element.id;
element.onclick = function(){
alert(id);
};
element = null;
}

  谨记及时将DOM或BOM的引用手动设为null。

gc root

  javascript的垃圾收集从gc root开始。gc root的核心包括当前执行环境的所有对象,可以沿着作用域链找到所有在当前环境的对象。然后从这些对象开始,沿着原型链一层一层地寻找引用的对象。这个过程类似函数执行的变量查找策略。也是先查作用域链,获得寻找的对象,然后查找该对象的原型链,获得要查找的属性。这是一种二维作用域链查找方式。这里的对象包括函数,因为在javascript里函数也是对象。

最佳实践

  在实际开发中,容易造成内存泄漏的主要有以下方面:无用的全局变量,闭包以及脱离dom tree的dom元素。

不用的变量及时赋null

  养成好习惯,在函数执行过程中将用完的变量设为null,使得对象脱离执行环境。

注意闭包

  闭包往往使得原本应该脱离执行环境的变量仍然保留在闭包函数的[[scope]]中而不能被释放,在使用闭包时应该谨慎,留意内存泄漏的发生,将作用域链里无用的变量设为null。

不要保留dom元素的引用

  当我们保留了某子元素的引用时,即便其父元素脱离了文档,整个父元素及其所有子元素的内存都不会释放。而且当处理dom元素和其回调函数时要注意不要将元素对象作为参数传入回调函数中,这样容易造成循环引用。虽然现代浏览器普遍已经不使用引用计数法,这个问题不大,但养成好习惯总是好的。

总结

  关于JavaScript的内存管理,只需要掌握两种垃圾回收策略即可。特别要注意闭包和DOM,BOM对象,注意循环引用。及时手动切断引用与DOM,BOM对象,全局环境下的对象的联系。