写点什么

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

  • 2021-06-01
  • 本文字数:3699 字

    阅读完需:约 12 分钟

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

写在前面


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


浏览器内核


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


浏览器渲染引擎Javascript 引擎
ChromeBlink(13 年之前使用的是 Safari 的 Webkit, Blink 是谷歌与欧朋一起搞的)V8
SafariWebkitJavaScriptCore
FirefoxGeckoSpiderMonkey--OdinMonkey
IETridentChakra


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



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



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


内存分配

简单说,栈内存,小且存储连续,操作起来简单方便,一般由系统自动分配,自动回收,所以文章内所说的垃圾回收,都是基于堆内存。

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


V8 中内存分类


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


概念:


  • 绝大部分的对象生命周期都很短,即存活时间很短

  • 生命周期很长的对象,基本都是常驻对象


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


垃圾回收

新生代


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


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


新生代算法为 Scavenge 算法,典型牺牲空间换时间的败家玩意,怎么说呢?首先他将新生代分为两个相等的半空间( semispace ) from space  与 to 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 的过程,在此类对象比较多的情况下,还是比较有优势的。



头图:Unsplash

作者:九渊

原文:https://mp.weixin.qq.com/s/2ARruErg3xNlvPOYjw7IiA

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

来源:政采云前端团队 - 微信公众号 [ID:Zoo-Team]

转载:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2021-06-01 08:003450

评论 7 条评论

发布
用户头像
大佬这句话我有点不确定是不是我想的这个意思,无法遍历到的对象会被标记,可以遍历到的是未被标记的对象,也就是将要被回收的对象,文章中这句话意思是标记的对象和未被标记的对象都会被复制到to space空间中吗?可是已经是被回收的对象了,还需要复制到to space里面去嘛,那不是更占地方了嘛,还是说文章这里写错了,只是把标记的对象放到to space

不可达对象(即无法遍历到的对象)将会被标记,并且复制未被标记的对象,放到 to space 中

2021-06-10 20:27
回复
大佬刚刚没转过弯,现已明白,哈哈
2021-06-10 20:34
回复
用户头像
厉害了,想知道楼主是从哪里学来的哦?
2021-06-02 17:24
回复
v8.dev
2021-07-18 16:42
回复
用户头像
隐藏类:V8优化性能的策略,运行期间,JS引擎会将创建的对象与隐藏类关联起来,以跟踪它们的属性特征。
function Test() {
this.text = 'wording';
}


const a = new Test();
const b = new Test();

由于两个实例共享同一个构造函数和原型,所以 a 和 b 共享相同的隐藏类;
但是如果动态地进行属性添加或者删除, a 和 b 便会对应两个不同的隐藏类,如果频繁地进行上述属性的操作,便有可能造成性能上的影响。
展开
2021-06-02 17:17
回复
用户头像
感谢分享!想问下有啥学习chrome源码的途径吗?
ps:那就没办法了,原则上是[解]决不了这个问题的(少个字)
2021-06-02 16:59
回复
没有更多了
发现更多内容

怎么判断自己适不适合做大数据培训

小谷哥

Amazon Braket 与量子计算

亚马逊云科技 (Amazon Web Services)

量子计算 Hero 专栏 Amazon Braket

Zebec流支付生态,开启多链布局的“两手准备”

鳄鱼视界

MatrixOne从入门到实战04——MatrixOne的连接和建表

MatrixOrigin

数据库 分布式 MatrixOrigin MatrixOne

关于平台工程的开发者工具链,你还想加点啥?

阿里巴巴云原生

阿里云 微服务 云原生 EDAS

如何通过 NFTScan 浏览器查询 NFT项目的 Metadata 数据【教程】

NFT Research

区块链 NFT

哪些人适合参加前端培训?

小谷哥

阿里云云原生加速器成员企业袋鼠云创始人陈吉平:深耕国产自研数字化技术与服务,持续为客户创造价值

阿里巴巴云原生

阿里云 云原生

低代码开发是未来软件开发的主流模式

元年技术洞察

低代码 方舟PaaS

开源让这位00后逆袭成为各类大奖收割者

OpenI启智社区

开源 OpenI启智社区 免费算力

云原生系列 二【轻松入门容器基础操作】

叶秋学长

云原生 沙箱实验 11月月更

前端自学能学会吗,培训怎么学

小谷哥

MatrixOne从入门到实践05——数据类型介绍

MatrixOrigin

数据库 分布式 MatrixOrigin MatrixOne

元器件科普 | 无源元件之——电容基础知识(超详细)

元器件秋姐

元器件采购 华秋商城 电容 电容器 电解电容器

Serverless Devs 社区联合信通院邀请您参加 2022 中国 Serverless 用户调查

阿里巴巴云原生

阿里云 Serverless 云原生

5步法助力自动化转型

FunTester

10分钟为你全面解答HDFS的SecondaryNamenode的作用

好程序员IT教育

大数据 hdfs

Stack Memory vs Heap Memory in Java

Mahipal_Nehra

Java heap memory Stack memory Java development

BANI时代下PMO如何求得生存?

PMO实践

项目管理 PMO 2022

Chrome 103支持使用本地字体,纯前端导出PDF优化

葡萄城技术团队

chrome 前端 HTTP PDF

如何通过Java 合并和取消合并 Excel 单元格

在下毛毛雨

Java Excel 合并单元格

专利解析|多维建模结合AI识别商品特征的方法

元年技术洞察

AI 数字化转型

年终最重磅!云原生实时数仓 SelectDB 首次产品发布等你来约!

SelectDB

数据库 云计算 大数据 实时计算

分布式存储之 etcd 的集群管理

焱融科技

云计算 分布式系统 etcd 高性能 分布式存储

帮助中心:培养客户自助服务意识的实用工具

Baklib

DevData Talks | 知乎艾辉:从工具建设到运营,千人团队研发提效最佳实践

思码逸研发效能

研发管理 研发效能

Hire Remote Developers

Mahipal_Nehra

Java angular blockchain React app development

数据监控预警系统,实现不同端信息推送

葡萄城技术团队

前端 数据可视化

《数据》杂志 | 浅析《网络安全法》修改对数据合规与隐私计算的影响

洞见科技

在大数据培训学习中怎么成为优秀的程序员

小谷哥

学历低可以参加大数据培训吗

小谷哥

V8 引擎垃圾回收与内存分配_语言 & 开发_政采云前端团队_InfoQ精选文章