一文理解JavaScript中的垃圾回收机制GC

513 阅读6分钟

本文档已更新于 【前端橘子君】 【Github】

可达性

可达性是指内存中的值通过某种方式进行访问,该值被长期放置在内存中。

如:

var obj = {
  name: '张三',
  age: 12,
  other: {
    height: 180
  }
}

如上所展示,其中张三, 12, 180甚至他们所在的对象都是可达的,因为他们都被obj及其子孙变量(如:name, age, other, height)所引用,其他变量可以通过obj及其子孙变量访问到它们。我们称之为可达对象(又称为活跃对象)

如果此时我们将obj重新赋值为null,那么obj{ name: '张三', age: 12, other: { height: 180 } }的关联就会断开,此时其他变量无法通过obj访问到,那么此时他们就称之为不可达对象(又称为非活跃对象),即使他们相互引用(180height变量引用,height变量被other变量引用)。

可达性

可达性的更多知识可以参考:garbage collection

固有可达值

  • 全局的局部变量和参数
  • 当前嵌套调用链上的其他函数的变量和参数
  • 全局变量
  • 还有一些其他的,内部的

以上情形产生的值我们称为固有可达值,又被称之为

第一点和第三点都好理解,因为浏览器的全局变量可以简单理解为window,该变量在任何地方都是可以调用的,我们认为window可达,所以其内部的变量和函数也是可达的。

如果我们在控制台中创建一个函数,那么这个函数会自动挂载在window变量上,所以我们调用sum()sum()是一样的道理。

对于第二点的理解:

function sum (a, b) {
  return function (c) {
    return a + b + c;
  }
}
var a = sum(1, 2);
console.log(a(3)); // 输出6

因为sum是挂载在window上的,所以其参数及变量都是可达的,变量c通过a + b + c的方式进行了引用,所以c也被认为是可达的。

以上理解为个人理解,如有偏差,敬请指正,为感。

堆和栈

内存中含有堆内存栈内存,在JS中,引用类型(如:object,array,new Date等)的值都被放入堆内存中,而非引用类型(如:string,number等)的值都被放入栈内存中。

想要更加详细了解,可以参考:js堆栈的理解

内存针对堆内存栈内存的特点采用不同的回收方式及策略。

栈回收

栈内存回收相对而言比较简单,主要是:JS引擎通过向下移动ESP指针(栈指针)来销毁存放在栈空间中的执行上下文。记住这里是执行上下文,或者称之为(执行上下文环境)。

var a = 1;
function foo() {
  var b = 2;
  var c = 1001;
};

// 函数调用
foo();

栈就像是往一个桶中放石头,最新丢进去的石头我们称之为全局上下文环境,它是不会自动清理的,除非用户自己变比了页面。当一个函数被调用时,就会产生一个执行上下文环境,这个环境执行完毕后会自动清理,除非产生了闭包,也就是固有可达值中的第二点。

栈中的执行上下文是通过ESP指针来进行回收的。

回收(GC)过程:

1、当页面第一次进入时,产生全局上下文环境并进入栈中,ESP指针指向该环境;
2、当调用foo函数时,产生foo执行上下文环境并入栈,此时ESP指向foo执行上下文环境;
3、当foo函数执行完毕,ESP向下调整,指向全局上下文环境;
4、当有其他函数执行时,该函数会入栈,覆盖foo执行上下文环境所在的空间,并将ESP指针指向其;
5、执行完毕后重新向下调整,指向全局上下文环境;
6、页面关闭时,全局上下文环境销毁。

具体流程如下图所示:

栈回收

堆回收

堆回收的过程和栈回收的过程不同,不是通过ESP指针进行清理,而是经过了标记-清理-内存整理的过程。

因为绝大多数对象的生存周期非常短,小部分对象的生存周期非常长,所以V8针对该特点将堆空间分为了新生代空间和老生代空间。

新生代空间体积较小,通常为1-8M,不是完全固定,根据具体情况进行分配。
老生代空间体积较大,通常存放经常调用或体积较大的值。

针对两者特点,采用的回收方式也是不同的,但是大过程还是标记-清理-内存整理的过程。

新生代回收

新生代内存将其空间划分为:Eden区域Survivor区域,Survivor区域又分为From SpaceTo Space

回收(GC)过程:

1、当一个对象产生时,会进入新生代(大体积的对象会直接进入老生代)的Eden区域;
2、当一个上下执行环境执行完毕后,如果该对象仍然被引用,那么标记其为活跃对象,否则为非活跃对象;
3、当Eden区域区域内存耗尽时,将活跃对象按照进入的顺序将其移动至From SpaceEden区域剩下的就全部都是非活跃对象;
4、将Eden区域中的对象全部清空;
5、将Survivor区域Eden区域整体翻转;
6、新对象不断加入,当Eden区域内存耗尽,则重复2,3,4,5步,其中,如果第一次存活的对象继续存活,则将其放入To Space中;
7、经过两次CG仍然存活的对象我们认为其是生存周期长的对象,将其从To Space放入老生代空间

上述策略我们称之为对象晋升策略,算法叫做Scavenge 算法,新生代算法可以说没有内存整理阶段,因为对象进入Survivor区域时是有序的,所以不存在断层(即碎片)。

具体图示过程如下图:
新生代CG

老生代回收

老生代回收机制比新生代回收机制要简单,用的是标记-清除法。即当一个上下执行环境执行完毕后,如果该对象仍然被引用,那么标记其为活跃对象,否则为非活跃对象,当内存耗尽时,将非活跃对象进行清除,并对所有活跃对象重新排序,整理碎片。

参考文献

以上内容并不是说浏览器垃圾回收机制只采用了上述算法和策略,其实还有其他,只是说主要以上述过程为主。

以上内容为个人根据相关参考文献进行整理而成,如有偏差,敬请指正,为感。

1、garbage collection
2、V8之旅:垃圾回收器


更多相关文档,请见:

线上地址 【前端橘子君】

GitHub仓库【前端橘子君】