在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函数执行完毕,程序运行结束
总的来说,js中栈中的数据回收依靠ESP(记录当前执行状态的指针)的下移来消除栈中保存的的执行上下文。
2. 堆中的数据回收
上面我们知道了栈的回收机制,但是我们知道,上下文中引用类型的值保存在堆中(不知道的可以看我的上一篇博客)。也就是说person1和person2的指针所指向的地方的内存虽然没有用了,但却依然占着内存。
在v8中,堆分为新生代和老生代两个区域。
- 新生代存放的是生存时间短的对象,内存在1~8M之间,使用js中的副垃圾回收器。
- 老生代中存放着生成时间久的对象,内存容量较大,使用js中的主垃圾回收器。
工作流程如下:
- 标记空间中活动对象和非活动对象。所谓活动对象就是还在使用的对象,非活动对象就是可以进行垃圾回收的对象。
- 回收非活动对象所占据的内存。其实就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。
- 内存整理。一般来说,频繁回收对象后,内存中就会存在大量不连续空间,我们把这些不连续的内存空间称为内存碎片。当内存中出现了大量的内存碎片之后,如果需要分配较大连续内存的时候,就有可能出现内存不足的情况。所以最后一步需要整理这些内存碎片,但这步其实是可选的,因为有的垃圾回收器不会产生内存碎片,比如副垃圾回收器。
2.1 副垃圾回收器
使用Scavenge 算法,把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域,如下图所示:
新加入的对象放入对象区域,快满时会进行一次垃圾清理操作:
- 将对象区域中的垃圾进行标记
- 将存活对象有序的排列起来,完整的复制到空闲区域
- 将对象区域和空闲区域进行角色反转 这样的算法比较适合新生区这种空间不大的垃圾回收,因为复制的操作需要成本,空间越大,时间成本越高。
也正是因为新生区的空间不大,很容易占满整个区域,因此js对其采用了对象晋升策略,两次副垃圾回收后任然存在的对象会被移动到老生区中。
2.2 主垃圾回收器
除了从新生区晋升来的对象,一些较大的对象也会被直接分配到老生区。主垃圾回收器采用的是标记-清除算法进行回收,过程如下:
- 标记阶段:标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。其实就是对当前调用栈进行一个扫描的过程。
- 清除过程:
- 整理过程:
3. js执行过程中的垃圾回收
上面已经了解了js的垃圾回收机制,不过由于 JavaScript 是运行在单线程之上的,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿。
新生代的内存较小,回收较快,停顿的影响不大。
老生代的内存较大,占用线程的时间较长,为了降低老生代造成的卡顿现象,使用了增量标记算法。将一个完整的垃圾回收拆分成一个个小的垃圾回收,减小了卡顿的现象。
参考文献: 极客浏览器专栏