欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JavaScript垃圾收集机制及内存泄漏问题

程序员文章站 2022-07-14 18:33:43
...

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对象的引用,顺利减少其引用次数,确保正常回收其占用的内存。

相关标签: 内存泄露