v8 引擎垃圾回收机制

垃圾回收(garbage collection),这里的垃圾,指的是程序中不再需要的内存空间.

常见内存泄露场景

  1. 缓存

  2. 作用域未释放(闭包)

var leakArray = [];
exports.leak = function () {
    leakArray.push("leak" + Math.random());
}

模块在编译执行后形成的作用域因为模块缓存的原因,不被释放,每次调用 leak 方法,都会导致局部变量 leakArray 不停增加且不被释放. 3. 没有必要的全局变量

  1. 无效的 DOM 引用

//dom still exist
function click(){
    // 但是 button 变量的引用仍然在内存当中。
    const button = document.getElementById('button');
    button.click();
}

// 移除 button 元素
function removeBtn(){
    document.body.removeChild(document.getElementById('button'));
}
  1. 定时器未清除

常见内存泄露问题

  1. 无限制增长的数组

  2. 无限制设置属性和值

  3. 任何模块内的私有变量和方法均是永驻内存的 a= null

  4. 大循环,无 gc 机会

垃圾回收算法

垃圾回收算法: 最困难的问题是如何确定哪些内存空间是可以回收的,即哪些内存空间是程序不再需要的,这是一个不可判定问题(undecidable problem)。所谓不可判定,就是没有哪个垃圾回收算法可以确定程序中所有可以回收的内存空间。

垃圾回收算法都是基于mark-and-sweep:

  • root对象包括全局对象以及程序当前的执行堆栈;

  • 从root对象出发,遍历其所有子对象,能够通过遍历访问到的对象是可以访问的;

  • 其他不能遍历对象是不可访问的,其内存空间可以回收;

策略:分代式垃圾回收机制

如何判断回收内容

如何确定哪些内存需要回收,哪些内存不需要回收,这是垃圾回收期需要解决的最基本问题。我们可以这样假定,一个对象为活对象当且仅当它被一个根对象 或另一个活对象指向。根对象永远是活对象,它是被浏览器或V8所引用的对象。被局部变量所指向的对象也属于根对象,因为它们所在的作用域对象被视为根对 象。全局对象(Node中为global,浏览器中为window)自然是根对象。浏览器中的DOM元素也属于根对象。

回收流程

  • 采用多线程的方式进行垃圾回收,尽量避免对JavaScript本身的代码执行造成暂停;

  • 利用浏览器渲染页面的空闲时间进行垃圾回收;

  • 根据The Generational Hypothesis,大多数对象的生命周期非常短暂,因此可以将对象根据生命周期进行区分,生命周期短的对象与生命周期长的对象采用不同的方式进行垃圾回收;

  • 对内存空间进行整理,消除内存碎片化,最大化利用释放的内存空间;

新生代

新生代存活时间比较短对象 老生代存活时间较长的对象

新生代的对象主要通过 scavenge 算法进行垃圾回收, 在具体实现时主要采用 cheney 算法. cheney 算法是一种采用复制的方式实现的垃圾回收算法.它将内存一分为二,每个空间称为 Semispace. 这两个 Semispace 中一个处于使用,一个处于闲置.处于使用的称为 From,闲置的称之为 To.分配对象时先分配到 From,当开始进行垃圾回收时,检查 From 存活对象赋值到 To, 非存活被释放.然后互换位置.再次进行回收,发现被回收过直接晋升,或者发现 To 空间已经使用了超过25%. 它的缺点是只能使用堆内存的一半,这是一个典型的空间换时间的方法,但是新生代声明周期较短,恰恰就适合这个算法.

新生代 一分为二 from to

老生代 在 from 占满的时候 from 用完了 往to 里面切 没用的 清除调用 标记过的 往from 清空

新生代默认给32m 老生代mark-SWEEP & Mark-compact

from 和 to 转换次数大于五 也会转入老生代

老生代

v8 老生代主要采用标记清除算法 (Mark-Sweep) 和标记压缩算法( Mark-compact), 在用 Scavenge 不合适.一个是对象较多需要赋值量太大而且还是没能解决空间问题. Mark-Sweep 是标记清除, 标记哪些死亡的对象,然后清除. 但是清楚过后出现内存不连续的情况,所以我们要使用 Mark-compact,他是基于 Mark-Sweep 演变而来的,他先将活着的对象移到右边,移动完成后, 直接清理边界外的内存. 当 cpu 空间不足的时候会非常的高效.v8 后续还引入了延迟处理,增量处理, 并引入并行标记处理.

老生代中的空间很复杂,有如下几个空间:

enum AllocationSpace {
  // TODO(v8:7464): Actually map this space's memory as read-only.
  RO_SPACE,    // 不变的对象空间
  NEW_SPACE,   // 新生代用于 GC 复制算法的空间
  OLD_SPACE,   // 老生代常驻对象空间
  CODE_SPACE,  // 老生代代码对象空间
  MAP_SPACE,   // 老生代 map 对象
  LO_SPACE,    // 老生代大空间对象
  NEW_LO_SPACE,  // 新生代大空间对象

  FIRST_SPACE = RO_SPACE,
  LAST_SPACE = NEW_LO_SPACE,
  FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE,
  LAST_GROWABLE_PAGED_SPACE = MAP_SPACE
};

最后更新于