这一次,完全弄清楚V8垃圾回收的流程

本人在面试候选人的时候,即便一个刚毕业的前端,问他 javascript 中内存的分配,都能答出来栈内存、堆内存。可是再追问一下,堆内存到底是怎么分配的,80% 的面试者都回答不上来了。javascript

V8 对内存的分配

都知道,js对象是存放在堆内存中的,那么具体是放在哪里呢?前端

V8 引擎会把内存中的 堆内存 分为两块不一样的区域,一块称之为老生代(old generation),另外一块是新生代(young generation)java

即便同处 新生代 中的对象中,它们的等级也不一样,又进一步分为 初级(nursery)代 等级和 中级(intermediate)代 等级。程序员

能够类比红警这类游戏中,刚出生的美国大兵是一级兵,经历过一场激烈的战役以后,幸存的大兵会被提高到二级兵,再经历一场战役,会被提高到三级兵。面试

js 的初级代、中级代也是如此。编程

在 js 中,当一个对象第一次分配内存时,会被分配到 新生代 中的 初级(nursery)代,至关因而最弱鸡的一级兵。浏览器

这个对象,若是在第一轮的垃圾回收中幸存下来。那么,咱们把它的等级到 中级(intermediate)代,也就是晋升成为了 二级兵。多线程

若是再通过下一次垃圾回收,这个对象幸存下来,这时候咱们就会把这个对象,从中级(intermediate)代移动到老生代,也就是晋升成为了三级兵。并发

为啥 V8 要这么作呢?编程语言

在垃圾回收中有一个重要的概念:“代际假说”(The Generational Hypothesis)。就是说,大部分的 js 对象,都是炮灰,一轮垃圾回收后,基本上都不会幸存,在内存中存在的时间很短。换句话说,从垃圾回收的角度来看,不少对象一经分配内存空间随即就变成了不可访问的。如同下图所示:

既然,短命的js对象,和命久的 js对象有如此的差距,V8 中就把他们区分开,采用不一样的垃圾回收策略。

javascript 主线程在正常的执行的时候,占用的内存空间会不断的增加。增加就会触发一个极限,触发极限的时候,垃圾回收就被触发了。对于新生代和老生代, V8 分别有两种垃圾回收器去处理。

V8中两种垃圾回收器

V8 有两个垃圾回收器,一个是主垃圾回收器(Full Mark-Compact),一个是副垃圾回收器( Scavenge )。这两个垃圾回收器,是相互独立的。

主垃圾回收器主要负责老生区中的垃圾回收(也会负责一部分的新生代heap),副垃圾回收器重新生代中回收垃圾。

From-Space / To-Space

对于副垃圾回收器来讲,有两块内存空间比较相关: From-spaceTo-space

不要感到困惑,From-spaceTo-space 的概念,和 初级代中级代 的概念,不是同一个概念。

From-spaceTo-space 能够理解成,真实可操做的内存空间;初级代中级代 表示一个对象的等级。From-SpaceTo-Space 永远是一个是空的,一个是使用中的。即便同处在 From-Space 中的对象,有的对象多是初级代(一等兵),有的对象多是中级代(二级兵)。

接下来,介绍一下,副垃圾回收器的过程。

副垃圾回收器 步骤

第1步 打标

这一步,是为了判断这轮GC中哪些对象须要被回收。

如何判断呢?就是看这个对象能不能被找到。

打标首先从根部开始查找,也就是顶层的执行栈、全局的对象开始查找,而后查找对象的引用,而后是对象引用的引用,一层层递归的找。

若是一个对象能够被访问到,则认为这个对象是活的,不该该被回收;不然,就会被回收。

接下来,咱们开始一轮垃圾回收的过程。

第2步

第 2 步,V8 从 from-space 中,把那些不会被垃圾清理掉的对象,移动到 to-space。这意味着,只有小部分的对象不会被垃圾清理掉,成功撤离到 to-space。 剩在 from-space 中的大部分对象,当成炮灰销毁。

不要和 js 代码中的拷贝对象指针搞混了,这里是更底层的二进制数据。拷贝的过程,就是拷贝那一块内存中的二进制数据的过程。

这一步也称为 撤离步骤 evacuation step,这步结束后,内存中的状况以下图:

上图右侧,撤离到 to-space 中的对象,成功的在第一轮的垃圾回收中活了下来,给他们的小方块打上一个标记(也就是每一块的小圆圈),标志着它们从 初级代 晋升到了 中级代

第3步

接下来,第3步被称为更新引用指针。

咱们发现,js 中对象的引用指针,仍是引用到了旧的from-space 空间上,咱们须要更新这些引用到 to-space 空间上。

结束后,以下图所示:

第4步

接下来,咱们把 to-spacefrom-space 交换位置。to-space 移动到左侧,成为了 下一轮的 from-spacefrom-space 移动到右侧,成为了下一轮的 to-space

第二轮垃圾回收

第一轮垃圾回收以后, js 继续执行,会有一些新分配的 初级代 对象,被推入到了 from-space 空间中, 安置在上一轮的幸存老兵 中级代 对象后面,以下图红色箭头所示:

第二轮垃圾回收的过程,和第一轮相似,就不赘述了。

二轮的垃圾回收的关键点是:from-space 中的幸存老兵 中级代 会拷贝最右侧的 old generation , 晋升为 老生代 。刚加入的新兵 初级代 会被拷贝到 to-space , 也打上一个标志,晋升成为 中级代, 以下图所示:

副垃圾回收器 与 并发处理

现今,V8 在新生代垃圾回收中使用并发清理。

什么? 并发?

对,你没有听错。虽然 javascript 是单线程的语言,这仅仅意味着 javascript 的程序员写的代码大部分是在 单线程上面跑的。可是 javascript 语言的宿主环境,好比说 V8 引擎,它是 Javascript 的执行环境,它能够新建出不少线程出来,用来辅助 javascript 主线程的工做。咱们把这些其余的辅助的线程称为 辅助线程(helper), javascript 执行的线程是 主线程(main thread)

把幸存对象撤离到 to-space 的工做,是 主线程 和 辅助线程一块儿并发执行,是为了d最大限度的减小 GC 的时间。

  • 每一个辅助线程 和 主线程,会把活的对象都移动到 To-Space。在每一次尝试将活的对象移动到 To-Space 的时,必须确保原子操做。

  • 不一样的辅助线程,都有可能经过不一样的路径找到相同的对象,并尝试将这个对象移动到 To-Space;不管哪一个辅助线程成功移动对象到 To-Space ,都必须更新这个对象的指针。

副垃圾回收器 小结

  • 由于代际假说的理论,只有小部分的 js 对象是会幸存下来的,因此在副垃圾回收器中,只会撤离一小部分的对象,拷贝到to-space的空间中,其余大部分对象都通通销毁。

  • from-spaceto-space 只有一个在用,空间开销很大,典型的用空间换时间。

  • 辅助线程 并发的帮助撤离

主垃圾回收器

上文中提到,新生代的对象,若是连续二轮GC幸存,会被晋升到老生代。

接下来,咱们来看一下,老生代的对象是如何被 主垃圾回收器所处理的。

老生代的垃圾回收会经历下面几个过程:打标 ( marking ) 清扫 (sweeping) **、压缩 (compacting)

打标

打标的过程, 在上文副垃圾回收器中已经讲过了

清扫

打标以后,V8 知道有哪些对象是不会被访问到,也就是须要被回收的了。这些被回收的对象所占用的位置,人走茶凉,就空了下来,成为了一个空闲的位置。

V8 会管理这些空闲的位置,以便下次有新到对象来了,能够把新到对象安置在空间位置中。

V8 把这些空闲的位置,扫到一张叫 FreeList 的表中来记录,这个过程被称为清扫,清扫的过程可让一个辅助线程在后台静默的去作掉。

压缩

若是你了解计算机操做系统,必定了解 碎片 的概念。

压缩的意思是,咱们想把 内存中的数据,挤一挤,靠得紧凑一点,把他们中间的间隙——也就是碎片 ,合并成一个大一些的连续空间。这样下次来一个比较大的对象时,能够有充足的空间来存放。

辅助线程 并发处理

在主垃圾回收器中,一样存在多辅助线程来提高效率。

首先, 辅助线程 开始并发的去打标(marking)

接下来, 当 辅助线程(helper) 的工做作完了,主线程就会暂停执行,转而进行最后的打标记工做(finalize marking)开始清理工做 ( sweeping tasks )

最后的打标记工做是主线程会快速的从根部从新检查一下,看有 辅助线程 是否有遗漏的,确保全部的对象都正确的扫过了

若是检查完毕OK,主线程和一部分辅助线程齐心合力一块儿作 合并碎片(Compact)更新(update)的操做。

另一部分辅助线程,会去并发的执行 清扫 工做,并不会影响并行内存页的整理工做和 JavaScript 的执行。

当这些工做都作完了,主线程会从新开始执行代码。

空闲时垃圾回收器

对于 JavaScript 程序员来讲,咱们是没有办法直接操做垃圾回收器的。

为了解决这个问题, V8 提出了空闲时间的概念。咱们的页面跑在浏览器内,浏览器以每秒60帧的速度去执行一些动画,浏览器大约有16.6毫秒的时间去渲染动画的每一帧。

若是这些渲染的工做,提早完成了,那么浏览器在下一帧以前的空闲时间去触发垃圾回收器。

总结

V8 的垃圾回收器项目自立项以来已经走过了漫长的道路。向现有的垃圾回收器添加并行、并发和增量垃圾回收技术通过了不少年的努力,而且也已经取得了一些成效。

将大量的移动对象的任务转移到后台进行,大大减小了主线程暂停的时间,改善了页面卡顿,让动画,滚动和用户交互更加流畅。Scavenger 回收器将新生代的垃圾回收时间减小了大约 20% - 50%,空闲时垃圾回收器在 Gmail 网页应用空闲的时候将 JavaScript 堆内存减小了 45%。并发标记清理能够减小大型 WebGL 游戏的主线程暂停时间,最多能够减小 50%。

大部分 JavaScript 开发人员并不须要考虑垃圾回收,可是了解一些垃圾回收的内部原理,能够帮助你了解内存的使用状况,以及采起合适的编范式。好比:从 V8 堆内存的分代结构和垃圾回收器的角度来看,建立生命周期较短的对象的成本是很是低的,可是对于生命周期较长的对象来讲成本是比较高的。这些模式是适用于不少动态编程语言的,而不只仅是 JavaScript。

相关文章
相关标签/搜索