V8 的垃圾回收策略主要基于分代式垃圾回收机制,在 V8 中,将内存分为新生代和老生代,新生代的对象为存活时间较短的对象,老生代的对象为存活事件较长或常驻内存的对象。
V8 堆的整体大小等于新生代所用内存空间加上老生代的内存空间,而只能在启动时指定,意味着运行时无法自动扩充,如果超过了极限值,就会引起进程出错。
4.2 Scavenge 算法
在分代的基础上,新生代的对象主要通过 Scavenge 算法进行垃圾回收,在 Scavenge 具体实现中,主要采用了一种复制的方式的方法—— Cheney 算法。
Cheney 算法将堆内存一分为二,一个处于使用状态的空间叫 From 空间,一个处于闲置状态的空间称为 To 空间。分配对象时,先是在 From 空间中进行分配。
当开始进行垃圾回收时,会检查 From 空间中的存活对象,将其复制到 To 空间中,而非存活对象占用的空间将会被释放。完成复制后,From 空间和 To 空间的角色发生对换。
当一个对象经过多次复制后依然存活,他将会被认为是生命周期较长的对象,随后会被移动到老生代中,采用新的算法进行管理。
还有一种情况是,如果复制一个对象到 To 空间时,To 空间占用超过了 25%,则这个对象会被直接晋升到老生代空间中。
4.3 标记-清除和标记-整理算法
对于老生代中的对象,主要采用标记-清除和标记-整理算法。标记-清除 和前文提到的标记一样,与 Scavenge 算法相比,标记清除不会将内存空间划为两半,标记清除在标记阶段会标记活着的对象,而在内存回收阶段,它会清除没有被标记的对象。
而标记整理是为了解决标记清除后留下的内存碎片问题。
4.4 增量标记(Incremental Marking)算法
前面的三种算法,都需要将正在执行的 JavaScript 应用逻辑暂停下来,待垃圾回收完毕后再恢复。这种行为叫作“全停顿”(stop-the-world)。
在 V8 新生代的分代回收中,只收集新生代,而新生代通常配置较小,且存活对象较少,所以全停顿的影响不大,而老生代就相反了。
为了降低全部老生代全堆垃圾回收带来的停顿时间,V8将标记过程分为一个个的子标记过程,同时让垃圾回收标记和JS应用逻辑交替进行,直到标记阶段完成。
经过增量标记改进后,垃圾回收的最大停顿时间可以减少到原来的 1/6 左右。
五、内存泄漏
内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
六、内存泄漏的常见场景
6.1 缓存
文章前言部分就有说到,JS 开发者喜欢用对象的键值对来缓存函数的计算结果,但是缓存中存储的键越多,长期存活的对象也就越多,这将导致垃圾回收在进行扫描和整理时,对这些对象做无用功。
6.2 作用域未释放(闭包)
var leakArray = []; exports.leak = function () { leakArray.push("leak" + Math.random()); }
以上代码,模块在编译执行后形成的作用域因为模块缓存的原因,不被释放,每次调用 leak 方法,都会导致局部变量 leakArray 不停增加且不被释放。
闭包可以维持函数内部变量驻留内存,使其得不到释放。
6.3 没必要的全局变量
声明过多的全局变量,会导致变量常驻内存,要直到进程结束才能够释放内存。
6.4 无效的 DOM 引用
//dom still exist function click(){ // 但是 button 变量的引用仍然在内存当中。 const button = document.getElementById('button'); button.click(); } // 移除 button 元素 function removeBtn(){ document.body.removeChild(document.getElementById('button')); }
6.5 定时器未清除
// vue 的 mounted 或 react 的 componentDidMount componentDidMount() { setInterval(function () { // ...do something }, 1000) }
vue 或 react 的页面生命周期初始化时,定义了定时器,但是在离开页面后,未清除定时器,就会导致内存泄漏。
6.6 事件监听为清空
componentDidMount() { window.addEventListener("scroll", function () { // do something... }); }
同 6.5, 在页面生命周期初始化时,绑定了事件监听器,但在离开页面后,未清除事件监听器,同样也会导致内存泄漏。
七、内存泄漏优化
1.解除引用