垃圾回收机制
内存生命周期
垃圾回收其实就是高效利用内存的一个过程,那我们首先需要知道内存生命周期是什么:
- 内存分配:变量、函数、对象等生命阶段,JS自动进行内存分配
- 内存使用:变量、函数、对象的调用、使用阶段
- 内存回收:执行上下文退出,无用变量内存回收阶段
引用计数法
顾名思义,一个变量每被引用一次,计数+1,当计数为0时,说明没有被使用到,则可以清除
存在的问题
当变量A被变量B引用,同时变量B又引用了变量A时,造成了循环引用问题,此时会发生内存泄漏。
如何解决呢?可以在声明的变量不再使用以后,随手赋值null,手动清除
标记清除法
当变量进入执行上下文时,打标记进入;当变量离开执行上下文时,打标记离开。离开标记的变量会在下一次GC时进行清除
V8引擎GC方法
V8为了存储高效,将内存分为新生代和老生代 新生代:
- 存储常用或者临时变量,采用更快的扫描算法
- 将空间划分为几个不同用途/等级块,对空间有浪费
- 空间换时间,这样的效率更快
老生代:
- 新生代的这种算法只适合临时的变量存储场景,对于老生代并不适合
- 老生代采用了标记清除和标记整理算法
新生代Scavenge算法
主要是依靠新生代里两块划分的区域from-space和to-space:
- 标记活动对象和非活动对象
- 复制
from-space的活动对象到to-space - 释放
from-space的非活动对象的内存 - 将
from-space和to-space互换
那GC是如何知道当前对象是否还活动呢?通过可达性判断
- 从初始根对象(window,global)指针开始,向下搜索子节点
- 可以搜索到的变量则可达,打上标记
- 所有没有被打上标记的变量,则不可达,可以被GC回收
新生代变量可能存在老生代晋升:
- 新生代内存还分为两部分A和B
- A存储新分配的变量,当经历过一次GC以后,该变量如果还存在,则放进B中
- 再经过一次GC以后,如果B中的该变量还存在,则晋升到老生代
老生代标记清除和标记整理
标记清除:
- 对老生代变量进行标记扫描,标记活动对象
- 对老生代进行清除扫描,将不活动的未标记对象清除
- 几番操作下来,老生代内存区域会存在一个内存碎片的问题,导致磁盘存储利用效率下降,所以需要标记整理
标记整理:
- 将内存随便进行一端方向移动,进行整理
常见内存泄露
- 全局变量
- 可以在使用完毕以后,手动赋值null
- 闭包
- 未被清除的定时器
- 未清除的DOM元素引用(直接删除真实DOM,导致变量引用未释放)
const el = {
button: document.getElementById('button')
}
document.body.removeChild(el.button)
[1] 深入理解谷歌最强V8垃圾回收机制
[2] 深入浅出JS GC垃圾回收