JavaScript具有自动垃圾收集机制,也就是说执行环境会负责管理代码执行过程中使用的内存。在C,C++之类的语言中,开发人员的一项基本任务就是手动跟踪内存使用情况。这会造成很多问题,在编写JavaScript代码时,开发人员不用担心内存分配以及无用内存的回收问题,JS完全实现了自动管理。
垃圾收集机制原理:
找出那些不再继续使用的变量,然后释放其占用的内存。为此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间)周期性的执行这一函数。在浏览器实现,通常使用以下两种策略:
(一)标记清除
JS中最常用的垃圾收集方式即为标记清除。当变量进入环境(例如:在函数中声明变量)时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占有的内存,因为只要执行流进入相应的环境,就可能会用到它们,而当变量离开环境时,将其标记为“离开环境”。
垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中变量引用的变量的标记。在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成垃圾收集策略(或类似的策略),只不过垃圾收集的时间间隔有所不同。
(二)标记清除
另一种不太常见的垃圾收集策略引用计数。
引用计数的含义是跟踪记录每一个值被引用的次数。当声明了一个变量并一个引用类型的值赋给该变量时,则这个值的引用次数就加1,如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了零外一个值,则这个值的引用次数减1。当这个值的引用次数变为0时,它就会释放那些引用次数为0的值所占的内存。
但是这种策略面临一个很严重的问题:循环引用
循环引用:指对象A包含一个指向对象B的指针,而对象B中也包含一个指向对象A的引用
例如:
function problem(){
var objectA = new Object();
var objectB = new Object();
objectA.someOtherObject = objectB;
objectB.anotherObject = objectA;
}
objectA和objectB通过各自的属性相互引用,也就是说,这两个对象的引用次数都是2。在采用标记清除策略的实现中,由于函数执行之后,这两个对象都离开了作用域,因此这种相互引用不存在问题。但是在采用引用计数策略的实现中,当函数执行完毕之后,objectA和objectB还将继续存在因为它们的引用次数永远不是0。假如这个函数被重复调用多次,就会导致大量内存得不到回收。
在IE中,有一部分对象并不是原生的JavaScript对象。例如:其BOM和DOM中的对象就是使用C++以COM对象形式实现的,而COM对象的垃圾收集机制采用的就是引用计数策略。即使IE的JavaScript引擎是使用标记清除策略来实现的,但是JavaScript访问到的COM对象依然是基于引用计数策略的。也就是说只要IE中涉及COM对象,就存在循环引用的问题。下面举例说明存在的问题以及解决方法:
案例:
var element = document.getElementById("some_element");//获取到一个对象,属于引用型
var myObject = new Object();
myObject.element = element;
element.someObject = myObject();
这个例子中,在一个DOM 元素和一个原生JavaScript对象之间创建了循环引用。其中,变量myObject有一个名为element的属性指向element对象,而变量element也有一个属性名叫someObject回指myObject.由于存在这个循环引用,就是将这个例子中的DOM从页面中移除,它也永远不会被回收。
为了解决这个问题,我们可以在不使用它们的时候手工断开原生JavaScript对象与DOM元素之间的连接。
例如可以使用下面的代码消除前面例子创建的循环引用:
myObject.element = null;
element.someObject = null;
将变量的值设置为null意味着切断变量与它此前引用值之间的连接。当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存。
内存泄漏
由于IE9之前的版本对JS对象和COM对象使用不同的垃圾收集例程,因此闭包在IE的这些版本中会导致一些特殊的问题。如果闭包的作用域链保存着一个HTML元素,那么就意味着该元素无法被销毁,例如:
function assignHandler(){
var element = document.getElementById("someElement");
element.onclick = function(){
alert(element.id);
};
}
以上代码创建了一个作为element元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用。由于匿名函数中保存了一个对assignHandler()的活动对象的引用,因此就导致无法减少element 的引用次数。只要匿名函数存在,element的引用次数至少也为1,因此它所占用的内存就永远不会被回收。处理这个问题的方法只需要小小修改一下代码:
function assignHandler(){
var element = document.getElementById("someElement");
var id = element.id;
element.onclick = function(){
alert(id);
};
以上代码通过吧element id 的一个副本保存在变量中,并且在闭包中引用该变量消除了循环引用。但是仅仅做这一步还不能解决内存泄漏的问题。闭包会引用包含函数的整个活动对象,而其中包含element对象。即使闭包不直接引用element,包含函数的活动对象中也依然会保存一个引用。因此有必要将element变量设置为null,这样就能解除对DOM对象的引用,顺利减少其引用次数,确保正常回收其占用的内存。