Skip to content
字数
1974 字
阅读时间
8 分钟

垃圾收集器会定期找出那些不再继续使用的变量,然后释放其内存

对象

浏览器的垃圾回收机制针对于 局部变量 ,因为 全局变量 的生命周期结束是直到浏览器页面被卸载才会结束。

局部变量 只在函数的执行过程中存在,在这个过程中会为局部变量在堆栈上分配相应的空间,以存储他们的值,然后在函数中使用这些变量,直至函数执行结束
——闭包由于内部函数的原因,外部函数并不能算是结束

标记无用变量的方式

标记清除

从根部(在 JS 中就是全局对象)出发定时扫描内存中的对象。 凡是能从根部到达的对象,都是还需要使用的。 那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收

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

javascript
function test(){
var a = 10 ;             //被标记 ,进入环境 
var b = 20 ;             //被标记 ,进入环境
}
test();                     //执行完毕 之后 a、b又被标离开环境,被回收。

垃圾回收器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记(闭包)。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾回收器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。 到目前为止,IE9+、Firefox、Opera、Chrome、Safari 的 js 实现使用的都是标记清除的垃圾回收策略或类似的策略,只不过垃圾收集的时间间隔互不相同。

引用计数

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

javascript
function test(){
    var a = {} ;         //a的引用次数为0 
    var b = a ;         //a的引用次数加1,为1 
    var c =a;           //a的引用次数再加1,为2
    var b ={};          //a的引用次数减1,为1
}

Netscape Navigator3 是最早使用引用计数策略的浏览器,但很快它就遇到一个严重的问题:循环引用。循环引用指的是对象 A 中包含一个指向对象 B 的指针,而对象 B 中也包含一个指向对象 A 的引用。

javascript
function fn() {
    var a = {};
    var b = {};
    a.pro = b;
    b.pro = a;
}
fn();

以上代码 a 和 b 的引用次数都是 2,fn() 执行完毕后,两个对象都已经离开环境,在标记清除方式下是没有问题的,但是在引用计数策略下,因为 a 和 b 的引用次数不为 0,所以不会被垃圾回收器回收内存,如果 fn 函数被大量调用,就会造成内存泄露。在 IE7 与 IE8 上,内存直线上升。

我们知道,IE 中有一部分对象并不是原生 js 对象。例如,其内存泄露 DOM 和 BOM 中的对象就是使用 C++ 以 COM 对象的形式实现的,而 COM 对象的垃圾回收机制采用的就是引用计数策略。因此,即使 IE 的 js 引擎采用标记清除策略来实现,但 js 访问的 COM 对象依然是基于引用计数策略的。换句话说,只要在 IE 中涉及 COM 对象,就会存在循环引用的问题。

javascript
var element = document.getElementById("some_element");
var myObject = new Object();
myObject.e = element;
element.o = myObject;

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

举个栗子:

javascript
window.onload=function outerFunction(){
    var obj = document.getElementById("element");
    obj.onclick=function innerFunction(){};
};

这段代码看起来没什么问题,但是 obj 引用了 document.getElementById('element'),而 document.getElementById('element') 的 onclick 方法会引用外部环境中的变量,自然也包括 obj,是不是很隐蔽啊。(在比较新的浏览器中在移除 Node 的时候已经会移除其上的 event 了,但是在老的浏览器,特别是 ie 上会有这个 bug)

回收机制

v8 采用 分代式垃圾回收机制,将内存分为 新生代老生代 ,对新老生代采取不同的回收策略

新生代空间

其中的对象一般存活时间比较短,采用 scavenge算法 (清除、扫气

将内存空间分为 from空间to空间 ,新分配的对象会被放入到 from 空间中,当 from 空间被占满时,新生代 GC 算法就启动了

算法会检查 from 空间 中存活的对象并将其复制到 to 空间 中,如果有失活的对象则会销毁,当复制完成后将 from 空间 与 to 空间 互换,,gc 结束

晋升到老空间

  • 经历了一次 scavenge 算法(新生代算法)
  • 在 to 空间 的内存占比超过 25%

老生代空间

老生代对象一般存活时间长且数量也多

采用 标记清除算法 (深度优先搜索)和 标记压缩算法

当发生以下情况启动 标记清除算法:
● 某一个空间没有分块的时候
● 空间中对象超过一定限制
● 不能保证新生代中的对象移动到老生代中

标记清除算法会先遍历堆中的所有对象,然后标记活的对象,在标记完成后,销毁所有没有被标记的对象

销毁完成后会造成堆空间出现碎片的情况,当碎片超过一定限制后启动压缩算法:
——在压缩过程中,将存活的对象向堆中一端移动,直到所有对象移动完成,然后清理掉不需要的内存

贡献者

The avatar of contributor named as jiechen jiechen

页面历史

撰写