本人在面试候选人的时候,即使一个刚毕业的前端,问他 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-space
和 To-space
。
不要感到困惑,From-space
和 To-space
的概念,和 初级代
和 中级代
的概念,不是同一个概念。
From-space
和To-space
可以理解成,真实可操作的内存空间;初级代
和中级代
表示一个对象的等级。From-Space
和To-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-space
和 from-space
交换位置。to-space
移动到左侧,成为了 下一轮的 from-space
,from-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-space
和to-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,可以聊天聊地聊人生,请注明来自掘金以及要投递哪里的岗位