垃圾怎么进行回收的
内存空间分为栈和堆。栈中的数据是每当执行完一个函数的实行上下文时就会销毁,例如showName这个函数
showName() {
let name = '王大锤'
console.log(name)
}
执行这个函数的时候,js引擎会创建它的执行上下文,压入调用栈中。当执行完这个函数就会出栈,最后内存就销毁了。
而销毁堆中的内存就需要垃圾回收器来帮助了,我们来看另一个函数
function bar() {
let obj = {name: '王大锤'} //obj是指向堆中保存这个对象的内存的引用
}
执行bar函数的时候它的执行上下文会入栈,而函数里创建了一个对象,此时变量obj是一个引用类型变量,它指向堆中的一个内存地址,而堆中这个内存地址存放着{name: '王大锤'}这个数据。当bar函数执行上下文出栈的时候就销毁了obj变量,但是对象obj是指向堆内存中的一块地址引用,仍然在堆中存在没有被销毁。我们再看看V8引擎是如何销毁堆中的垃圾数据的。
新生代和老生代
V8引擎会把堆分成新生代和老生代两个区域。由V8引擎中的两个不同的垃圾回收器进行处理。
- 新生代:存放生存时间
短的对象。通常只支持1~8M容量。由V8引擎中的副垃圾回收器处理 - 老生代:存放生存时间
长的对象。支持容量比新生代大得多。由V8引擎中的主垃圾回收器处理
垃圾回收器工作流程
- 标记空间中活动对象(仍在使用)和非活动对象(可以进行回收)
- 标记完成后,回收非活动对象占据的内存
- 内存整理,频繁回收对象就会在内存中出现不连续空间(内存碎片),如下次需要分配较大连续内存的时候就会出现内存不足的情况。
(主垃圾回收器产生内存碎片,副垃圾回收器不产生内存碎片)
副垃圾回收器
小对象通常被分到新生区。虽然新生区空间不大,但是垃圾回收比较频繁。新生代采用了Scavenge算法:把新生区空间对半划分,一半是对象区域,一半是空闲区域。新加入对象存放到对象区域,当对象区域快满的时候就进行垃圾回收处理。垃圾清理过程:
- 垃圾标记
- 把存活对象复制到空闲区域,并有序排列起来,相当于完成了内存整理
- 复制完成后对象区域和空闲区域进行身份互换,原来的对象区域变成了空闲区域,空闲区域变成了对象区域,这种角色互换可以无限的进行重复
- 如果经过两次垃圾回收还存活的对象,移入老生区(这个过程称为
对象晋升策略)
主垃圾回收器
除了新生代晋升的对象,一些大的对象会直接分配到老生区。老生区对象有两个特点:占用空间大,存活时间长。
主垃圾回收器采用标记—清除的算法进行垃圾回收:标记从一组根元素开始,递归遍历,能到达的元素是活动对象,没到达的元素就是垃圾数据,没有了外界的引用,进行标记。如果某个对象已经没有了引用其的变量,那么就会被当作垃圾回收了。
对一块内存地址进行多次标记—清除算法会产生大量不连续内存碎片,这种情况需要另一种标记—整理算法来处理,标记过程和标记—清除算法一样,标记之后就让所有存活对象往内存块的一端移动,形成了连续的内存地址。
全停顿
js是运行在主线程的,一旦执行垃圾回收算法,就会对主线程进行阻塞,当垃圾回收完之后才恢复js脚本的运行,这种情况叫做全停顿。如果堆中内存过大,执行一次完整的垃圾回收可能需要的时间就到达1s以上,这样就会给页面造成了卡顿现象。一般出现全停顿现象的都是在老生代中,因为其中的对象都比较大,垃圾回收需要的时间长。为了降低这种卡顿现象,V8引擎采用了增量标记算法,让垃圾回收标记和js的逻辑交替进行,直到标记阶段完成,采取这种算法把一次大的垃圾回收分成了很多个小任务,进行穿插执行,这样就不会给页面造成卡顿现象了。