彻底弄清楚V8垃圾回收的流程

1,343 阅读11分钟

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

V8 对内存的分配

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

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

即使同处 新生代 中的对象中,它们的等级也不同,又进一步分为 初级(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。

字节跳动大大大大量量量量招人了

字节跳动(杭州|北京|上海)大量招人,福利超级棒,薪资水平秒杀 BAT,上班不打卡、每天下午茶、免费零食无限供应、免费三餐(我念下菜单,大闸蟹鲍鱼扇贝海鲜烤鱼片黑椒牛柳咖喱牛肉麻辣小龙虾)、免费健身房、入职配touch bar15寸顶配全新mbp、每月还有租房房补。 这次真的机会多多,年后研发人数要扩招n倍,技术氛围好,大牛多,加班少,还犹豫什么?快发简历到下方邮箱,就现在!

仅仅是一小部分的jd链接如下, 更多的欢迎加微信~

前端jd: job.toutiao.com/s/bJM4Anjob…

后端jd: job.toutiao.com/s/bJjjTsjob…

测试jd: job.toutiao.com/s/bJFv9bjob…

产品jd: job.toutiao.com/s/bJBgV8job…

前端实习生: job.toutiao.com/s/bJ6NjAjob…

后端实习生: job.toutiao.com/s/bJrjrkjob…

持续招聘大量前端、服务端、客户端、测试、产品,实习社招都阔以

简历发 dujuncheng@bytedance.com,建议加微信 dujuncheng1,可以聊天聊地聊人生,请注明来自掘金以及要投递哪里的岗位