V8 引擎垃圾回收与内存分配

这是第 82 篇不掺水的原创,想获取更多原创好文,请搜索公众号关注咱们吧~ 本文首发于政采云前端博客: V8 引擎垃圾回收与内存分配

写在前面

工欲善其事,必先利其器,本文之器非器具之器,乃容器也,言归正传,做为一个前端打工人,左手刚 const 定义常量,忠贞不二,转头就 new 几个对象,玩的火热,真是个优秀的 jser,风骚的操做背后,必有日夜不辍的 QWER,外加一个走 A,废话很少说,浏览器内核是啥玩意?还不知道都有啥浏览器内核?那就先来看看浏览器内核。前端

浏览器内核

提到浏览器内核,Blink、Weikit、Gecko、Trident 张口就来,这些只是各个浏览器内核的组成部分之一渲染引擎,对应的还有 JavaScript引擎,简单罗列一下:算法

浏览器 渲染引擎 Javascript 引擎
Chrome Blink(13 年以前使用的是 Safari 的 Webkit, Blink 是谷歌与欧朋一块儿搞的) V8
Safari Webkit JavaScriptCore
Firefox Gecko SpiderMonkey--OdinMonkey
IE Trident Chakra

渲染引擎和 JS 引擎相互协做,打造出浏览器显示的页面,看下图:npm

图片

简单看看就行,不重要,既然是讲垃圾回收( Garbage Collection 简称 GC ),那就要先去回收站了,回收站有个学名叫:内存,计算机五大硬件之一存储器的核心之一,见下图:浏览器

图片

说句更不重要的,JS 是没有能力管理内存和垃圾回收的,一切都要依赖各个浏览器的 JS 引擎,因此为了逼格更高一点,就不要说 JS 垃圾回收了,你看,我说 V8 垃圾回收,是否是厉害多了(摸了摸愈来愈没有阻力的脑壳)。缓存

内存分配

简单说,栈内存,小且存储连续,操做起来简单方便,通常由系统自动分配,自动回收,因此文章内所说的垃圾回收,都是基于堆内存。并发

堆内存,大(相对栈来讲)且不连续。app

V8 中内存分类

在讲内存分配以前,先了解一下弱分代假说,V8 的垃圾回收主要创建在这个假说之上。ide

概念:post

  • 绝大部分的对象生命周期都很短,即存活时间很短
  • 生命周期很长的对象,基本都是常驻对象

基于以上两个概念,将内存分为新生代 (new space)老生代 (old space)两个区域。划重点,记一下。性能

垃圾回收

新生代

新生代(32 位系统分配 16M 的内存空间,64 位系统翻倍 32M,不一样浏览器可能不一样,可是应该差不了多少)。

新生代对应存活时间很短的假说概念,这个空间的操做,很是频繁,绝大多数对象在这里经历一次生死轮回,基本消亡,没消亡的会晋升至老生代内。

新生代算法为 Scavenge 算法,典型牺牲空间换时间的败家玩意,怎么说呢?首先他将新生代分为两个相等的半空间( semispace ) from spaceto space,来看看这个败家玩意,是怎么操做的,他使用宽度优先算法,是宽度优先,记住了不。两个空间,同一时间内,只会有一个空间在工做( from space ),另外一个在休息( to space )。

  1. 首先,V8 引擎中的垃圾回收器检测到 from space 空间快达到上限了,此时要进行一次垃圾回收了
  2. 而后,从根部开始遍历,不可达对象(即没法遍历到的对象)将会被标记,而且复制未被标记的对象,放到 to space 中
  3. 最后,清除 from space 中的数据,同时将 from space 置为空闲状态,即变成 to space,相应的 to space 变成 from space,俗称翻转

图片

也是,你说空间都给他了,他爱咋地处理就咋地处理呗,总不可能强迫王校长开二手奥拓吧,固然了,对于小对象,这么来一次,时间的优点那是杠杠的,虽然浪费了一半空间,可是问题不大,能 hold 住。

固然优秀的 V8 是不可能容忍,一个对象来回的在 form space 和 to space 中蹦跶的,当经历一次 form => to 翻转以后,发现某些未被标记的对象竟然还在,会直接扔到老生代里面去,好似后浪参加比赛,晋级了,优秀的嘞。

除了上面一种状况,还有一个状况也会晋级,当一个对象,在被复制的时候,大于 to space 空间的 25% 的时候,也会晋级了,这种自带背景的选手,那是不敢动的,直接晋级到老生代。

老生代

老生代( 32 位操做系统分配大约 700M 内存空间,64 位翻倍 1.4G,同样,每一个浏览器可能会有差别,可是差不了多少)。

老生代比起新生代但是要复杂的多,所谓能者多劳,空间大了,责任就大了,老生代能够分为如下几个区域:

  • old object space 即你们口中的老生代,不是所有老生代,这里的对象大部分是由新生代晋升而来
  • large object space 大对象存储区域,其余区域没法存储下的对象会被放在这里,基本是超过 1M 的对象,这种对象不会在新生代对象中分配,直接存放到这里,固然了,这么大的数据,复制成本很高,基本就是在这里等待命运的降临不可能接受仅仅是知其然,而不知其因此然
  • Map space 这个玩意,就是存储对象的映射关系的,其实就是隐藏类,啥是隐藏类?就不告诉你(不知道的大佬已经去百度了)
  • code space 简单点说,就是存放代码的地方,编译以后的代码,是根据大佬们写的代码编译出来的代码

看个图,休息一下:

图片

讲了这么多基本概念,聊聊最后的老生代回收算法,老生代回收算法为:标记和清除/整理(mark-sweep/mark-compact)。

在标记的过程当中,引入了概念:三色标记法,三色为:

  • 白:未被标记的对象,即不可达对象(没有扫描到的对象),可回收
  • 灰:已被标记的对象(可达对象),可是对象尚未被扫描完,不可回收
  • 黑:已被扫描完(可达对象),不可回收

固然,既然要标记,就须要提供记录的坑位,在 V8 中分配的每个内存页中建立了一个 marking bitmap 坑位。

大体的流程为:

  1. 首先将全部的非根部对象所有标记为白色,而后使用深度优先遍历,是深度优先哈,和新生代不同哈,按深度优先搜索沿途遍历,将访问到的对象,直接压入栈中,同时将标记结果放在 marking bitmap (灰色) 中,一个对象遍历完成,直接出栈,同时在 marking bitmap 中记录为黑色,直到栈空为止,来张图,休息一下

图片

  1. 标记完成后,接下来就是等待垃圾回收器来清除了,清除完了以后,会在原来的内存区域留下一大堆不连续的空间,小对象还好说,这个时候若是来一个稍微大一点的对象,没有内存能够放的下这个傻大个了,怎么办?只能触发 GC,可是吧,原来清除的不连续的空间加起来又能够放的下这个傻大个,很惋惜啊,启动一次 GC 性能上也是嗖嗖的往下掉啊;V8 能允许这样的事发生?确定不存在嘛!
  2. 因此在清除完以后,新生代中对象,再一次分配到老生带而且内存不足的时候,会优先触发标记整理(mark-compact), 在标记结束后,他会将可达对象(黑色),移到内存的另外一端,其余的内存空间就不会被占用,直接释放,等下次再有对象晋升的时候,轻松放下。

看到这里各位大佬可能会有疑问,那要是我 GC 搞完以后,再来个对象,满了咋办,你说咋办,直接崩好很差,这个时候就须要大佬们写代码的时候,要珍惜内存了,对内存就像珍惜你的女友同样,啥?没有女友? 那就没办法了,原则上是决不了这个问题的。

基本的内存和垃圾回收是交代完了,其中还有一些概念,仍是要说一下的,接着往下看!

图片

写屏障

想一个问题,当 GC 想回收新生代中的内容的时候,某些对象,只有一个指针指向了他,好巧不巧的是,这个指针仍是老生代那边对象指过来的,怎么搞?我想回收这个玩意,难道要遍历一下老生代中的对象吗?这不是开玩笑吗?为了回收这一个玩意,我须要遍历整个老生代,代价着实太大,搞不起,搞不起,那怎么办哩?

V8 引擎中有个概念称做写屏障,在写入对象的地方有个缓存列表,这个列表内记录了全部老生代指向新生代的状况,固然了新生成的对象,并不会被记录,只有老生代指向新生代的对象,才会被写入这个缓存列表。

在新生代中触发 GC 遇到这样的对象的时候,会首先读一下缓存列表,这相比遍历老生代全部的对象,代价实在是过小了,这操做值得一波 666,很优秀,固然了,关于 V8 引擎内在的优化,还有不少不少,各位大佬能够慢慢去了解。

全停顿(stop-the-world)

关于全停顿,本没有必要单独来说,可是,I happy 就 good。

在以往,新/老生带都包括在内,为了保证逻辑和垃圾回收的状况不一致,须要中止 JS 的运行,专门来遍历去遍历/复制,标记/清除,这个停顿就是:全停顿。

这就比较恶心了,新生代也就算了,自己内存不大,时间上也不明显,可是在老生代中,若是遍历的对象太多,太大,用户在此时,是有可能明显感到页面卡顿的,体验嘎嘎差。

因此在 V8 引擎在名为 Orinoco 项目中,作了三个事情,固然只针对老生代,新生代这个后浪仍是能够的,效率贼拉的高,优化空间不大。三个事情分别是:

  • 增量标记

将原来一口气去标记的事情,作成分步去作,每次内存占用达到必定的量或者屡次进入写屏障的时候,就暂时中止 JS 程序,作一次最多几十毫秒的标记 marking,当下次 GC 的时候,反正前面都标记好了,开始清除就好了

  • 并行回收

从字面意思看并行,就是在一次全量垃圾回收的过程当中,就是 V8 引擎经过开启若干辅助线程,一块儿来清除垃圾,能够极大的减小垃圾回收的时间,很优秀,手动点赞

  • 并发回收

并发就是在 JS 主线程运行的时候,同时开启辅助线程,清理和主线程没有任何逻辑关系的垃圾,固然,须要写屏障来保障

小结

V8 引擎作的优化有不少,还有好比屡次( 2 次)在新生代中可以存活下来的对象,会被记录下来,在下次 GC 的时候,会被直接晋升到老生代,还有好比新晋升的对象,直接标记为黑色,这是由于新晋升的对象存活下来的几率很是高,这两种状况就算是再也不使用,再下下次的时候也会被清除掉,影响不大,可是这个过程,第一种就省了新生代中的一次复制轮回,第二种就省了 marking 的过程,在此类对象比较多的状况下,仍是比较有优点的。

最后一句

终于,写完了,原本想着写的更详细一些,可是那样篇幅会很大,下次吧,有机会的话再写写 V8 执行的过程或者 V8 建立对象都干了些啥玩意什么什么的,其实 V8 引擎(或者各个 JS 引擎)这个东西太庞大了,我了解的也是冰山一角,因此文章确定有不许确的地方,欢迎大佬们严正指正,积极交流。

推荐阅读

初级工程师如何快速成长和寻求突破

npm 私库从搭建到数据迁移最后容灾备份的一些解决方案

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 40 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在平常的业务对接以外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推进并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

若是你想改变一直被事折腾,但愿开始能折腾事;若是你想改变一直被告诫须要多些想法,却无从破局;若是你想改变你有能力去作成那个结果,却不须要你;若是你想改变你想作成的事须要一个团队去支撑,但没你带人的位置;若是你想改变既定的节奏,将会是“5 年工做时间 3 年工做经验”;若是你想改变原本悟性不错,但老是有那一层窗户纸的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但愿参与到随着业务腾飞的过程,亲手推进一个有着深刻的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我以为咱们该聊聊。任什么时候间,等着你写点什么,发给 ZooTeam@cai-inc.com

相关文章
相关标签/搜索