v8垃圾回收机制

Posted by Youzi on June 18, 2021

V8引擎垃圾回收机制

JS执行引擎自带有垃圾回收,且是分片定期执行的,很少需要开发者手动回收;可能在浏览器端不是特别需要注意变量释放等内容;但在nodejs端,运行内存有限(64位系统在1400MB,32位是700MB),所以需要额外关注垃圾回收。

为什么要设置内存限制:首先是因为JS单线程执行机制,导致垃圾回收和代码运行不能处于同个时间段,否则必然造成冲突;其次因为JS垃圾回收是很耗时的,切片任务做一次小的垃圾回收需要50ms,做一次全量的垃圾回收需要1s以上,所以V8选择直接限制堆内存的使用,防止出现操作太大内存导致的垃圾回收压力;

nodejs调整占用内存的大小:

1
2
3
4
5
// 这是调整老生代这部分的内存,单位是MB。后面会详细介绍新生代和老生代内存
node --max-old-space-size=2048 xxx.js 

// 这是调整新生代这部分的内存,单位是 KB。
node --max-new-space-size=2048 xxx.js

JS内存划分

JS对内存的申请有两种,一种是栈内存,一种是堆内存;

  • 基本类型:会存放在栈内存中,可以直接通过值来访问;
  • 引用类型:由于引用类型的大小不固定,栈内存中存放的是堆内存的地址,也就是指针,指向堆内存;

判断变量是否可回收

标记清除

变量进入执行上下文时,将变量标记为“进入环境”,正常逻辑是不能释放标记为“进入环境”的变量的,既然存在执行上下午中,就表示该变量可能会被用到;当变量离开执行上下文时,则标记为“离开环境”。

变量的标记方式有很多种,如何标记变量不是十分重要,关键在于采取何种策略:

  1. 垃圾收集器在运行时会给存储在内存中的所有变量都打上标记;
  2. 找到执行上下文中的变量,以及被这些变量引用的变量,把它们的标记去掉;
  3. 执行上一步之后,剩下的还带有标记的变量,就被视为准备删除的变量,因为在当前执行环境中已经无法访问到这些变量了;
  4. 垃圾收集器完成内存清除工作,销毁标记的值,并回收内存空间。

目前大部分浏览器用的都是标记清除式的垃圾回收策略;只是执行垃圾回收策略的时间间隔有所不同。

引用计数

引用计数不太常见,因为循环引用的bug会导致有的变量长期驻留在内存中;

其策略是为每个变量跟踪记录一个被引用的次数,声明变量并赋值时,这个值的引用次数就是1;后续如果该值又被赋值给其他变量,则自增,反之如果引用这个值的变量改变了引用的对象,则自减;当引用计数变成0时,就表示没有变量引用,自然就没有办法访问到了,就会被垃圾收集器回收了;

为什么说循环引用会导致bug:

1
2
3
4
const o1 = {}
const o2 = {}
o1.a = o2
o2.b = o1

如果用引用计数法,这俩变量互相引用,计数都是1,不会被回收;

为什么说事件绑定执行完之后要解绑

1
2
3
4
5
const el = document.querySelector('div')
el.onclick = function (event) {
  // do something
  event.el === el // true
}

这也是循环引用的常见实例,这就可能造成内存泄漏,所以在不需要该事件绑定时解除绑定;

垃圾回收策略

首先明确垃圾回收策略不是只有一种,而是根据内存的划分来选择不同的策略;

V8将内存分为两种类型:新生代和老生代;

新生代

这一部分存放生命周期较短的变量,一个对象最开始都会被分配到新生代(如果新生代空间不够,也会分配到老生代),满足一定条件后会通过晋升算法移动到老生代;默认情况下新生代32位系统下是16MB,64位double;

而新生代又被分为两块大小相等的内存空间,称为semispace,其中一块正在使用的空间称为from,闲置的空间称为to,这个回收算法称为Cheney算法;

策略:

  • 执行GC时,用标记清除法对from空间进行检查,将存活的对象复制到to空间;
  • 清空from空间,交换from - to空间。

这种算法优势在于,只会复制存活的对象,而且由于新生代空间存放的都是生命周期短的对象,所以真正存活的对象很少,所以在时间复杂度上有优势;另外这种算法是典型的空间换时间,因为空间的利用率始终是50%;由于这种特性,这类算法不能用于空间比较大的垃圾回收中。

晋升

当一个对象经过多次垃圾回收算法,仍然存活在新生代空间内,它就会被认为是生命周期较长的对象,这类对象会被移动到老生代中,用其他算法进行管理;

晋升条件:

  • from复制到to时,会检查它的内存地址,判断这个对象是否经历过回收,如果经历过就复制到老生代中;
  • 当要从from复制到to时,如果to空间已经使用超过25%,会将这个对象直接晋升到老生代中;

老生代

由于Cheney算法的一些弊端,在老生代的管理中,V8用了Mark-SweepMark-Compact相结合的方式;

mark-sweep

指的是标记清除法,分为标记阶段和清除阶段;

  • 标记阶段

从根节点出发,标记可达的对象(可达对象指从根节点出发,使用图的遍历算法,标记所有可获取到值的对象);

  • 清除阶段

遍历堆内存,清除所有未被标记的对象,意味着清除所有不可达的对象;

弊端:这种算法会导致清除后的内存空间不连续,会出现一段内存是占据的,一段内存是空的,所以为了防止这种碎片化空间,就有了mark-compact算法;

mark-compact

相比标记清除,标记整理在清除阶段时,会将标记过的内存空间,全部移动到内存空间的一侧,然后直接清理掉另一侧的空间,这样就做到了占据的内存空间是连续的。

弊端:由于这种算法需要移动对象,所以会产生额外的时间复杂度,在执行上不会很快,所以V8主要使用的是mark-sweep,在空间不足以对晋升的对象进行分配时,才会使用mark-compact