V8垃圾回收机制

警告
本文最后更新于 2022-09-25,文中内容可能已过时。

这部分的阅读资料非常多,整理了下面几个文章,有空常看看。对主要内容做了简单的整理。

  1. 引用计数就是判断对象被引用的次数,为 0 时回收,大于 0 就不回收。

缺点: 对象循环引用的时候,就会产生不被回收的情况,从而造成内存泄露。比如:

js

function fn () {
  const obj1 = {}
  const obj2 = {}
  obj1.a = obj2
  obj2.a = obj1
}
fn()
  1. 标记清除将可达对象标记起来,不可达的对象当垃圾回收。

什么是可达,其实就是树,根节点是全局对象,从根开始向下搜索子节点,在这个树上的能被搜索到就是可达的。


垃圾回收不仅仅是上面的两个算法。

JS 中内存的流程:分配内存 –> 使用内存 –> 释放内存

  • 基础类型:分配固定内存的大小,值存在 栈内存
  • 引用类型:分配内存大小不固定,指针存在 栈内存中,指向 堆内存 中的对象,通过引用来访问

栈内存存储的基础类型大小固定,所以栈内存都是 操作系统自动分配和释放回收的
堆内存存储的引用类型的大小不固定,所以需要 JS 引擎来手动释放


64 位约 1.4G/1464MB , 32 位约 0.7G/732MB

之所以做这个限制:

  1. 是因为浏览器上没有大内存的场景,
  2. 是因为 V8 的垃圾回收机制的限制 – 清理大量的内存会非常消耗时间,会阻塞单线程的 JS,性能和体验直线下降

JS 中对象存活周期有两种,短期和长期的,一个对象经过了多次垃圾回收机制它还存在那就变成长期的了,如果对这种明知收不掉还每次都去做回收的动作就很消耗性能。
– 因此,V8 将堆分为了两个空间:新生代和老生代。新生代是存放存活周期短对象的地方,老生代是存放存活周期长对象的地方

  • 新生代:
    • 大小 1-8M
    • 副垃圾回收器 + Scavenge 算法
  • 老生代:
    • 大得多
    • 主垃圾回收器 + Mark-Sweep && Mark-Compact 算法

Scavenge 算法是一个典型的牺牲空间换取时间的复制算法,在占用空间不大的场景上非常适用。
Scavange 算法将新生代堆分为两部分,分别叫 from-space 和 to-space。工作方式也很简单,就是将 from-space 中存活的活动对象复制到 to-space 中,并将这些对象的内存有序的排列起来,然后将 from-space 中的非活动对象的内存进行释放,完成之后,将 from space 和 to space 进行互换,这样可以使得新生代中的这两块区域可以重复利用。

一个对象第一次被分配到 nursery 子代,经过一次回收后还存在新生代就被移动到 intermediate 子代,再下一次还存在就会被 副垃圾回收期 移动到老生代中。

不和新生代使用一样的算法是因为 老生代 空间大,不适合复制,浪费性能,再说老生代就是大量活着的,来回复制没必要。所以才使用了另外两种标记清理算法。

因为垃圾回收优先于代码执行,老生代的回收有可能插上卡顿现象,为了解决这个问题,V8 进行了优化,项目代号为 orinoco。 提出了 增量标记,惰性清理,并发,并行 的优化方法,详细见下面参考文章吧。