一文搞懂V8引擎的垃圾回收机制
前言
我们平时在写代码的过程中,好像很少需要自己手动进行垃圾回收,那么V8是如何来减少内存占用,从而避免内存溢出而导致程序崩溃的情况的。为了更高效地回收垃圾,V8引入了两个垃圾回收器,它们分别针对不同场景进行工作。
如果这篇文章有帮助到你,️关注+点赞️鼓励一下作者,文章公众号首发,关注 前端南玖 第一时间获取最新文章~
垃圾从何而来
我们先来搞清楚这些‘垃圾’是怎么产生的
不管使用哪一种语言,我们势必都会频繁的操作数据,这些数据一般是存放在栈内存与堆内存中,通常是会在内存中创建一块空间,使用这块空间,再不需要的时候回收这块空间。
比如:
var test = {}
test.a = new Array(100)
当执行这段代码时,先会为全局对象(window)添加一个test属性,并在堆内存中创建一个空对象,并将该对象的地址指向test属性,随后又创建了一个长度为100的数组,并将该数组地址指向了test.a的属性值。
从上图我们可以看出,栈中保存了指向window对象的指针,通过栈中window的地址可以找到window对象,通过window对象可以找到test对象,通过test对象可以找到a数组。
如果此时,我们将a属性指向了另一个对象:
test.a = {}
那么此时的内存会变成这样:
那么这个时候堆内存中的数组其实就变成了‘垃圾数据’,因为我们再也访问不到它了,不过我们不必担心它会一直占用内存,因为V8中的垃圾回收器会帮我们自动清理。
对于 JavaScript 而言,也正是这个“自动”释放资源的特性带来了很多困惑,也让一些 JavaScript 开发者误以为可以不关心内存管理,这是一个很大的误解。
代际假说与分代收集
代纪假说是垃圾回收领域中的一个重要术语,后续垃圾回收策略都是建立在该假说之上的。
特点
第一个是大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经分配内存,很快就变得不可访问
第二个是不死的对象,会活得更久
为了达到最好的回收效果,V8会根据对象的生存周期的不同来应用不同的回收算法,所以在 V8 中会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象
新生区通常只支持 1~8M 的容量,而老生区支持的容量就大很多了。对于这两块区域,V8 分别使用两个不同的垃圾回收器,以便更高效地实施垃圾回收
副垃圾回收器,主要负责新生代的垃圾回收
主垃圾回收器,主要负责老生代的垃圾回收
垃圾回收器的工作流程
V8的内存结构
新生代(new_space):大多数的对象开始都会被分配在这里,这个区域相对较小但是垃圾回收特别频繁,该区域被分为两半,一半用来分配内存,另一半用于在垃圾回收时将需要保留的对象复制过来。老生代(old_space):新生代中的对象在存活一段时间后就会被转移到老生代内存区,相对于新生代该内存区域的垃圾回收频率较低。老生代又分为老生代指针区和老生代数据区,前者包含大多数可能存在指向其他对象的指针的对象,后者只保存原始数据对象,这些对象没有指向其他对象的指针。大对象区(large_object_space):存放体积超越其他区域大小的对象,每个对象都会有自己的内存,垃圾回收不会移动大对象区。代码区(code_space):代码对象,会被分配在这里,唯一拥有执行权限的内存区域。map区(map_space):存放Cell和Map,每个区域都是存放相同大小的元素,结构简单
垃圾回收的过程一般主要出现在新生代与老生代。
垃圾回收策略
标记清除
标记清除( Mark-Sweep ),目前在 JavaScript引擎 里这种算法是最常用的,到目前为止的大多数浏览器的 JavaScript引擎 都在采用标记清除算法,只是各大浏览器厂商还对此算法进行了优化加工,且不同浏览器的 JavaScript引擎 在运行垃圾回收的频率上有所差异。 此算法分为 标记 和 清除 两个阶段,标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁。
引擎在执行 GC(使用标记清除算法)时,需要从出发点去遍历内存中所有的对象去打标记,而这个出发点有很多,我们称之为一组根对象,而所谓的根对象,其实在浏览器环境中包括又不止于 全局Window对象、文档DOM树等。
整个标记清除算法大致过程就像下面这样:
- 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0;
- 然后从各个根对象开始遍历,把不是垃圾的节点改成1;
- 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间;
- 最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收;
优点:
实现比较简单,打标记也无非打与不打两种情况,这使得一位二进制位(0和1)就可以为其标记,非常简单
缺点:
在清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了 内存碎片,并且由于剩余空闲内存不是一整块,它是由不同大小内存组成的内存列表,这就牵扯出了内存分配的问题
引用计数
引用计数( Reference Counting ),这其实是早先的一种垃圾回收算法,它把对象是否不再需要简化定义为对象有没有其他对象引用到它,如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收,但因为它的问题很多,目前很少使用这种算法了。
它的策略是跟踪记录每个变量值被使用的次数
- 当声明了一个变量并且将一个引用类型赋值给该变量的时候这个值的引用次数就为 1;
- 如果同一个值又被赋给另一个变量,那么引用数加 1;
- 如果该变量的值被其他的值覆盖了,则引用次数减 1;
- 当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存;
优点:
- 引用计数在引用值为 0 时,也就是在变成垃圾的那一刻就会被回收,所以它可以立即回收垃圾;
- 标记清除算法需要每隔一段时间进行一次,那在应用程序(JS脚本)运行过程中线程就必须要暂停去执行一段时间的 GC,另外,标记清除算法需要遍历堆里的活动以及非活动对象来清除,而引用计数则只需要在引用时计数就可以了;
缺点:
- 需要一个计数器,而此计数器需要占很大的位置,因为我们也不知道被引用数量的上限;
- 无法解决循环引用无法回收的问题;
工作流程
不论什么类型的垃圾回收器,它们都有一套相同的执行流程
第一步是标记空间中活动对象和非活动对象。所谓活动对象就是还在使用的对象,非活动对象就是可以进行垃圾回收的对象。
第二步是回收非活动对象所占据的内存。其实就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。
第三步是做内存整理。一般来说,频繁回收对象后,内存中就会存在大量不连续空间,我们把这些不连续的内存空间称为内存碎片。当内存中出现了大量的内存碎片之后,如果需要分配较大连续内存的时候,就有可能出现内存不足的情况。所以最后一步需要整理这些内存碎片,但这步其实是可选的,因为有的垃圾回收器不会产生内存碎片,比如接下来我们要介绍的副垃圾回收器。
副垃圾回收器
副垃圾回收器主要负责新生区的垃圾回收。而通常情况下,大多数小的对象都会被分配到新生区,所以说这个区域虽然不大,但是垃圾回收还是比较频繁的。
新生代中用 Scavenge 算法来处理。所谓 Scavenge 算法,是把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域,如下图所示:
新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作。
在垃圾回收过程中,首先要对对象区域中的垃圾做标记;标记完成之后,就进入垃圾清理阶段,副垃圾回收器会把这些存活的对象复制到空闲区域中,同时它还会把这些对象有序地排列起来,所以这个复制过程,也就相当于完成了内存整理操作,复制后空闲区域就没有内存碎片了。完成复制后,对象区域与空闲区域进行角色翻转,也就是原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域。这样就完成了垃圾对象的回收操作,同时这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去。
由于新生代中采用的 Scavenge 算法,所以每次执行清理操作时,都需要将存活的对象从对象区域复制到空闲区域。但复制操作需要时间成本,如果新生区空间设置得太大了,那么每次清理的时间就会过久,所以为了执行效率,一般新生区的空间会被设置得比较小。也正是因为新生区的空间不大,所以很容易被存活的对象装满整个区域。为了解决这个问题,JavaScript 引擎采用了对象晋升策略,也就是经过两次垃圾回收依然还存活的对象,会被移动到老生区中。
主垃圾回收器
主垃圾回收器主要负责老生区中的垃圾回收。除了新生区中晋升的对象,一些大的对象会直接被分配到老生区。因此老生区中的对象有两个特点,一个是对象占用空间大,另一个是对象存活时间长。
由于老生区的对象比较大,若要在老生区中使用 Scavenge 算法进行垃圾回收,复制这些大的对象将会花费比较多的时间,从而导致回收执行效率不高,同时还会浪费一半的空间。因而,主垃圾回收器是采用**标记 - 清除(Mark-Sweep)**的算法进行垃圾回收的。
它的原理就是:
- 首先是标记过程阶段。标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。
- 接下来就是垃圾的清除过程。它和副垃圾回收器的垃圾清除过程完全不同,对一块内存多次执行标记 - 清除算法后,可能会产生大量不连续的内存碎片。
- 而碎片过多会导致大对象无法分配到足够的连续内存,于是又产生了另外一种算法——标记 - 整理(Mark-Compact),这个标记过程仍然与标记 - 清除算法里的是一样的,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
全停顿
由于 JavaScript 是运行在主线程之上的,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(Stop-The-World)。
在 V8 新生代的垃圾回收中,因其空间较小,且存活对象较少,所以全停顿的影响不大,但老生代就不一样了。如果在执行垃圾回收的过程中,占用主线程时间过久,将会造成页面卡顿。
为了降低老生代的垃圾回收而造成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,我们把这个算法称为增量标记(Incremental Marking) 算法。
一文搞懂V8引擎的垃圾回收机制的更多相关文章
- 一文搞懂V8引擎的垃圾回收
引言 作为目前最流行的JavaScript引擎,V8引擎从出现的那一刻起便广泛受到人们的关注,我们知道,JavaScript可以高效地运行在浏览器和Nodejs这两大宿主环境中,也是因为背后有强大的V ...
- V8 引擎的垃圾回收机制
V8 引擎将内存分为新生代和老生代 由于不同对象的生存周期不同,只用一种回收策略来解决问题,这样效率会很低.所以V8采用了一种代回收的策略,将内存分为两个生代:新生代(new generation)和 ...
- V8引擎的垃圾回收策略
V8 的垃圾回收策略主要基于分代式垃圾回收机制.所谓分代式,就是将内存空间分为新生代和老生代两种,然后采用不同的回收算法进行回收. 新生代空间 新生代空间中的对象为存活时间较短的对象,大多数的对象被分 ...
- V8 下的垃圾回收机制
V8 实现了准确式 GC,GC 算法采用了分代式垃圾回收机制.因此,V8 将内存(堆)分为新生代和老生代两部分. 1.新生代算法 新生代中的对象一般存活时间较短,使用 Scavenge GC 算法. ...
- 浅谈Chrome V8引擎中的垃圾回收机制
垃圾回收器 JavaScript的垃圾回收器 JavaScript使用垃圾回收机制来自动管理内存.垃圾回收是一把双刃剑,其好处是可以大幅简化程序的内存管理代码,降低程序员的负担,减少因 长时间运转而带 ...
- 浅谈V8引擎中的垃圾回收机制
最近在看<深入浅出nodejs>关于V8垃圾回收机制的章节,转自:http://blog.segmentfault.com/skyinlayer/1190000000440270 这篇文章 ...
- 一文搞懂RAM、ROM、SDRAM、DRAM、DDR、flash等存储介质
一文搞懂RAM.ROM.SDRAM.DRAM.DDR.flash等存储介质 存储介质基本分类:ROM和RAM RAM:随机访问存储器(Random Access Memory),易失性.是与CPU直接 ...
- 基础篇|一文搞懂RNN(循环神经网络)
基础篇|一文搞懂RNN(循环神经网络) https://mp.weixin.qq.com/s/va1gmavl2ZESgnM7biORQg 神经网络基础 神经网络可以当做是能够拟合任意函数的黑盒子,只 ...
- Chrome V8系列--浅析Chrome V8引擎中的垃圾回收机制和内存泄露优化策略
V8 实现了准确式 GC,GC 算法采用了分代式垃圾回收机制.因此,V8 将内存(堆)分为新生代和老生代两部分. 一.前言 V8的垃圾回收机制:JavaScript使用垃圾回收机制来自动管理内存.垃圾 ...
- 一文搞懂 Prometheus 的直方图
原文链接:一文搞懂 Prometheus 的直方图 Prometheus 中提供了四种指标类型(参考:Prometheus 的指标类型),其中直方图(Histogram)和摘要(Summary)是最复 ...
随机推荐
- 1.HVV介绍
HVV介绍 1.护网职责划分 红队:打点人员.攻击人员.社工人员 蓝队:监控人员.研判人员.溯源人员 2.护网需要具备的技能 红队: 外围打点能力.漏洞挖掘能力.漏洞分析能力.权限提升能力.权限维持能 ...
- Delete ␍eslint(prettierprettier)错误
Delete ␍eslint(prettier/prettier)错误 今天在用HBuilder开发uniapp项目时,想换成vscode进行开发,但是用vscode打开之前的项目,eslint报错一 ...
- MQTT(EMQX) - Java 调用 MQTT Demo 代码
POM <dependency> <groupId>org.eclipse.paho</groupId> <artifactId>org.eclipse ...
- 开源不到 48 小时获 35k star 的推荐算法「GitHub 热点速览」
本周的热点除了 GPT 各类衍生品之外,还多了一个被马斯克预告过.在愚人节开源出来的推特推荐算法,开源不到 2 天就有了 35k+ 的 star,有意思的是,除了推荐算法本身之外,阅读源码的工程师们甚 ...
- TypeScript必知三部曲(一)TypeScript编译方案以及IDE对TS的类型检查
TypeScript代码的编译过程一直以来会给很多小伙伴造成困扰,typescript官方提供tsc对ts代码进行编译,babel也表示能够编译ts代码,它们二者的区别是什么?我们应该选择哪种方案?为 ...
- 从Chat-GPT看爆火技术概念及医疗领域科技与应用场景
作者:京东健康 陈刚 一.前言 最近OpenAI在官网上宣告了多模态大模型 GPT-4 的诞生,它可能是迄今为止最好的多模态模型. 主要更新内容如下: 1. 逻辑分析能力更加全面.「考试」能力大幅提升 ...
- 【LeetCode动态规划#11】打家劫舍系列题(涉及环结构和树形DP的讨论)
打家劫舍 力扣题目链接(opens new window) 你是一个专业的小偷,计划偷窃沿街的房屋.每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻 ...
- AspectCore和MSDI 实现Name注册以及解析对象
AspectCore 在注册服务这块比较简单,默认是无法根据Name去注册和解析对象,这边做一下这块的扩展 大致原理是根据自定义Name去生成对应的动态类型,然后使用委托或者对象的方式,进行注册 ti ...
- [UR #14]人类补完计划
计数好题. 题意:给定简单无向图 \(G=(V,E),|V|=n,|E|=m\),有 \(n\leq 16,m\leq {n\choose 2}\),求所有为基环树的子图的权值之和.一个基环树的权值定 ...
- 【漏洞分析】ReflectionToken BEVO代币攻击事件分析
前言 BEVO代币是一种Reflection Token(反射型代币),并且拥有通缩的特性.关于Reflection Token更为详细的说明可参考这篇文章.然后目前浏览到的很多分析报告没有指出其漏洞 ...