JS学习笔记之垃圾回收机制

465 阅读6分钟

Javascript垃圾回收机制的原理

JavaScript具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。在编写JavaScript程序时,开发人员不用再关心内存使用问题,所需内存的分配以及无用内存的回收完全实现了自动管理。这种垃圾收集机制的原理其实很简单:找出那些不再继续使用的变量,然后释放其占用的内存。 为此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间),周期性地执行这一操作。因此垃圾收集器必须跟踪哪个变量有用哪个变量没用,对于不再有用的变量打上标记,以备将来收回其占用的内存。用于标识无用变量的策略可能会因实现而异,但具体到浏览器中的实现,则通常有两个策略。

一、标记清除

JavaScript中最常用的垃圾收集方式是标记清除。当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”。

垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。

到2008年为止,IE、Firefox、Opera、Chrome和Safari的JavaScript实现使用的都是标记清除式的垃圾收集策略(或类似的策略),只不过垃圾收集的时间间隔互有不同。

二、引用计数

引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的内存。

但很快这种方法就遇到了一个严重的问题:循环引用。循环引用指的是对象A中包含一个指向对象B的指针,而对象B中也包含一个指向对象A的引用。请看下面这个例子:

function p(){
            var objA = new Object();
            var objB = new Object();
            objA.a = objB;
            objB.b = objA;
        }

Snipaste_2021-06-06_21-31-49.png 当 objB 这个变量执行 objB 这个对象时,objB 这个对象的引用计数会加 1,此时引用计数值为 1,接下来 objA 的 a 属性又指向了 objB 这个对象,所以此时 objB 这个对象的引用计数为 2。同理 objA 这个对象的引用计数也为2。

当代码执行完后,会将变量 objA 和 objB 赋值为 null,但是此时 objA 和 objB 这两个对象的引用计数都为1,并不为 0(个人理解:如果实在不理解可以看箭头方向,箭头到达端表示那个对象被引用,可以看到objA 被引用了两次,箭头起始端对这个值引用的变量又取得了另外一个值,表示改变,由图可知只改变一次,所以最终代码执行完毕的引用计数为2-1=1次), 所以并不会进行垃圾回收,但是这两个对象已经没有作用了,在函数外部也不可能使用到它们,假如这个函数被重复多次调用,就会导致大量内存得不到回收。

我们知道,IE中有一部分对象并不是原生JavaScript对象。例如,其BOM和DOM中的对象就是使用C++以COM(Component Object Model,组件对象模型)对象的形式实现的,而COM对象的垃圾收集机制采用的就是引用计数策略。因此,即使IE的JavaScript引擎是使用标记清除策略来实现的,但JavaScript访问的COM对象依然是基于引用计数策略的。换句话说,只要在IE中涉及COM对象,就会存在循环引用的问题。下面这个简单的例子,展示了使用COM对象导致的循环引用问题:

        var element = document.getElementById("some_element");
        var myObject = new Object();
        myObject.element = element;
        element.someObject = myObject;

这个例子在一个DOM元素(element)与一个原生JavaScript对象 (myObject)之间创建了循环引用。其中,变量myObject有一个名为element的属性指向element对象;而变量element也有一个属性名叫someObject回指myObject。由于存在这个循环引用,即使将例子中的DOM从页面中移除,它也永远不会被回收。

为了避免类似这样的循环引用问题,最好是在不使用它们的时候手工断开原生JavaScript对象与DOM元素之间的连接。例如,可以使用下面的代码消除前面例子创建的循环引用:

        myObject.element = null;
        element.someObject = null;

将变量设置为null意味着切断变量与它此前引用的值之间的连接。当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存。 为了解决上述问题,IE9把BOM和DOM对象都转换成了真正的JavaScript对象。这样,就避免了两种垃圾收集算法并存导致的问题,也消除了常见的内存泄漏现象。

小结:

  1. JavaScript是一门具有自动垃圾收集机制的编程语言,开发人员不必关心内存分配和回收问题。
  2. 离开作用域的值将被自动标记为可以回收,因此将在垃圾收集期间被删除。
  3. “标记清除”是目前主流的垃圾收集算法,这种算法的思想是给当前不使用的值加上标记,然后再回收其内存。
  4. 另一种垃圾收集算法是“引用计数”,这种算法的思想是跟踪记录所有值被引用的次数。JavaScript引擎目前都不再使用这种算法;但在IE中访问非原生JavaScript对象(如DOM元素)时,这种算法仍然可能会导致问题。
  5. 当代码中存在循环引用现象时,“引用计数”算法就会导致问题。
  6. 解除变量的引用不仅有助于消除循环引用现象,而且对垃圾收集也有好处。为了确保有效地回收内存,应该及时解除不再使用的全局对象、全局对象属性以及循环引用变量的引用