js的垃圾回收机制

380 阅读4分钟

在js中原始数据类型存储在栈空间中,引用类型存储在堆空间中。通过这种分配方案解决了数据分配的问题。

但是在数据被使用完之后,可能就不再需要了,这种数据称为垃圾数据。垃圾数据如果不进行回收会越堆越多,很新运,js内部有一套自己的垃圾回收机制,因此不需要你对它进行相关的操作。那么我们来看看js到底是如何运作的吧!

1. 调用栈中的数据回收

var age = 20
function foo() {
    var age1 = 21
    var person1 = {name: "小猪皮皮呆"}
    function showName () {
        var age2 = 22
        var person2 = { name: "小猪" }
    }
    showName()
}
foo()
console.log(age)

执行流程如下:

  • 执行全局代码,生成全局执行上下文,压入调用栈
  • 执行foo函数,生成foo函数执行上下文,压入调用栈
  • 执行showName函数,生成showName函数执行上下文,压入调用栈
  • showName函数执行完毕,调用栈内部指针ESP指向foo函数执行上下文
  • foo函数执行完毕,调用栈内部指针ESP指向全局执行上下文
  • console函数执行完毕,程序运行结束

image.png 总的来说,js中栈中的数据回收依靠ESP(记录当前执行状态的指针)的下移来消除栈中保存的的执行上下文。

2. 堆中的数据回收

上面我们知道了栈的回收机制,但是我们知道,上下文中引用类型的值保存在堆中(不知道的可以看我的上一篇博客)。也就是说person1和person2的指针所指向的地方的内存虽然没有用了,但却依然占着内存。

在v8中,堆分为新生代老生代两个区域。

  • 新生代存放的是生存时间短的对象,内存在1~8M之间,使用js中的副垃圾回收器
  • 老生代中存放着生成时间久的对象,内存容量较大,使用js中的主垃圾回收器

工作流程如下:

  • 标记空间中活动对象和非活动对象。所谓活动对象就是还在使用的对象,非活动对象就是可以进行垃圾回收的对象。
  • 回收非活动对象所占据的内存。其实就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。
  • 内存整理。一般来说,频繁回收对象后,内存中就会存在大量不连续空间,我们把这些不连续的内存空间称为内存碎片。当内存中出现了大量的内存碎片之后,如果需要分配较大连续内存的时候,就有可能出现内存不足的情况。所以最后一步需要整理这些内存碎片,但这步其实是可选的,因为有的垃圾回收器不会产生内存碎片,比如副垃圾回收器。

2.1 副垃圾回收器

使用Scavenge 算法,把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域,如下图所示:

image.png

新加入的对象放入对象区域,快满时会进行一次垃圾清理操作:

  • 将对象区域中的垃圾进行标记
  • 将存活对象有序的排列起来,完整的复制到空闲区域
  • 将对象区域和空闲区域进行角色反转 这样的算法比较适合新生区这种空间不大的垃圾回收,因为复制的操作需要成本,空间越大,时间成本越高。

也正是因为新生区的空间不大,很容易占满整个区域,因此js对其采用了对象晋升策略,两次副垃圾回收后任然存在的对象会被移动到老生区中。

2.2 主垃圾回收器

除了从新生区晋升来的对象,一些较大的对象也会被直接分配到老生区。主垃圾回收器采用的是标记-清除算法进行回收,过程如下:

  • 标记阶段:标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。其实就是对当前调用栈进行一个扫描的过程。
  • 清除过程:

image.png

  • 整理过程:

image.png

3. js执行过程中的垃圾回收

上面已经了解了js的垃圾回收机制,不过由于 JavaScript 是运行在单线程之上的,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿

新生代的内存较小,回收较快,停顿的影响不大。

老生代的内存较大,占用线程的时间较长,为了降低老生代造成的卡顿现象,使用了增量标记算法。将一个完整的垃圾回收拆分成一个个小的垃圾回收,减小了卡顿的现象。

参考文献: 极客浏览器专栏