声明:本文为掘金首发签约文章,未经受权禁止转载。前端
咱们知道垃圾回收机制是引擎来作的,JS引擎有不少种(各个浏览器都不一样),其垃圾回收机制在一些细节及优化上略有不一样,本文咱们以一些通用的回收算法做为切入,再由 V8 引擎发展至今对该机制的优化为例(为何以 V8 为例?由于它市场占有率大 😄 ),一步一步深刻来助咱们了解垃圾回收机制,由于只有真正了解垃圾回收机制,后面才能理解内存泄漏的问题以及手动预防和优化程序员
JavaScript 是门魅力无限的语言,关于它的 GC(垃圾回收)方面,你了解多少呢?想来大部分人是由于面试才去看一些面试题从而了解的垃圾回收,那在正式开始以前,给你们列几个小问题,你们能够先想一下答案,带着问题及答案再去看文章,最后读完此文若是你的答案能够优化,即有收获面试
什么是垃圾回收机制?算法
垃圾是怎样产生的?数组
为何要进行垃圾回收?浏览器
垃圾回收是怎样进行的?markdown
V8 引擎对垃圾回收进行了哪些优化?并发
固然,咱们可不只仅是为了面试,其目的是一次性完全搞懂 GC!假如你对其中某块内容不太理解,不要着急,先读完整篇文章了解内容再回过头来仔细看一遍就会清晰不少,干货满满,先赞后看哦函数
GC
即 Garbage Collection
,程序工做过程当中会产生不少 垃圾
,这些垃圾是程序不用的内存或者是以前用过了,之后不会再用的内存空间,而 GC
就是负责回收垃圾的,由于他工做在引擎内部,因此对于咱们前端来讲,GC
过程是相对比较无感的,这一套引擎执行而对咱们又相对无感的操做也就是常说的 垃圾回收机制
了性能
固然也不是全部语言都有 GC
,通常的高级语言里面会自带 GC
,好比 Java、Python、JavaScript
等,也有无 GC
的语言,好比 C、C++
等,那这种就须要咱们程序员手动管理内存了,相对比较麻烦
咱们知道写代码时建立一个基本类型、对象、函数……都是须要占用内存的,可是咱们并不关注这些,由于这是引擎为咱们分配的,咱们不须要显式手动的去分配内存
可是,你有没有想过,当咱们再也不须要某个东西时会发生什么?JavaScript 引擎又是如何发现并清理它的呢?
咱们举个简单的例子
let test = {
name: "isboyjc"
};
test = [1,2,3,4,5]
复制代码
如上所示,咱们假设它是一个完整的程序代码
咱们知道 JavaScript
的引用数据类型是保存在堆内存中的,而后在栈内存中保存一个对堆内存中实际对象的引用,因此,JavaScript
中对引用数据类型的操做都是操做对象的引用而不是实际的对象。能够简单理解为,栈内存中保存了一个地址,这个地址和堆内存中的实际值是相关的
那上面代码首先咱们声明了一个变量 test
,它引用了对象 {name: 'isboyjc'}
,接着咱们把这个变量从新赋值了一个数组对象,也就变成了该变量引用了一个数组,那么以前的对象引用关系就没有了,以下图
没有了引用关系,也就是无用的对象,这个时候假如任由它搁置,一个两个还好,多了的话内存也会受不了,因此就须要被清理(回收)
用官方一点的话说,程序的运行须要内存,只要程序提出要求,操做系统或者运行时就必须提供内存,那么对于持续运行的服务进程,必需要及时释放内存,不然,内存占用愈来愈高,轻则影响系统性能,重则就会致使进程崩溃
在 JavaScript 内存管理中有一个概念叫作 可达性
,就是那些以某种方式可访问或者说可用的值,它们被保证存储在内存中,反之不可访问则需回收
至于如何回收,其实就是怎样发现这些不可达的对象(垃圾)它并给予清理的问题, JavaScript
垃圾回收机制的原理说白了也就是按期找出那些再也不用到的内存(变量),而后释放其内存
你可能还会好奇为何不是实时的找出无用内存并释放呢?其实很简单,实时开销太大了
咱们均可以 Get 到这之中的重点,那就是怎样找出所谓的垃圾?
这个流程就涉及到了一些算法策略,有不少种方式,咱们简单介绍两个最多见的
策略
标记清除(Mark-Sweep),目前在 JavaScript引擎
里这种算法是最经常使用的,到目前为止的大多数浏览器的 JavaScript引擎
都在采用标记清除算法,只是各大浏览器厂商还对此算法进行了优化加工,且不一样浏览器的 JavaScript引擎
在运行垃圾回收的频率上有所差别
就像它的名字同样,此算法分为 标记
和 清除
两个阶段,标记阶段即为全部活动对象作上标记,清除阶段则把没有标记(也就是非活动对象)销毁
你可能会疑惑怎么给变量加标记?其实有不少种办法,好比当变量进入执行环境时,反转某一位(经过一个二进制字符来表示标记),又或者能够维护进入环境变量和离开环境变量这样两个列表,能够自由的把变量从一个列表转移到另外一个列表,当前还有不少其余办法。其实,怎样标记对咱们来讲并不重要,重要的是其策略
引擎在执行 GC(使用标记清除算法)时,须要从出发点去遍历内存中全部的对象去打标记,而这个出发点有不少,咱们称之为一组 根
对象,而所谓的根对象,其实在浏览器环境中包括又不止于 全局Window对象
、文档DOM树
等
整个标记清除算法大体过程就像下面这样
优势
标记清除算法的优势只有一个,那就是实现比较简单,打标记也无非打与不打两种状况,这使得一位二进制位(0和1)就能够为其标记,很是简单
缺点
标记清除算法有一个很大的缺点,就是在清除以后,剩余的对象内存位置是不变的,也会致使空闲内存空间是不连续的,出现了 内存碎片
(以下图),而且因为剩余空闲内存不是一整块,它是由不一样大小内存组成的内存列表,这就牵扯出了内存分配的问题
假设咱们新建对象分配内存时须要大小为 size
,因为空闲内存是间断的、不连续的,则须要对空闲内存列表进行一次单向遍历找出大于等于 size
的块才能为其分配(以下图)
那如何找到合适的块呢?咱们能够采起下面三种分配策略
First-fit
,找到大于等于 size
的块当即返回
Best-fit
,遍历整个空闲列表,返回大于等于 size
的最小分块
Worst-fit
,遍历整个空闲列表,找到最大的分块,而后切成两部分,一部分 size
大小,并将该部分返回
这三种策略里面 Worst-fit
的空间利用率看起来是最合理,但实际上切分以后会形成更多的小块,造成内存碎片,因此不推荐使用,对于 First-fit
和 Best-fit
来讲,考虑到分配的速度和效率 First-fit
是更为明智的选择
综上所述,标记清除算法或者说策略就有两个很明显的缺点
First-fit
策略,其操做还是一个 O(n)
的操做,最坏状况是每次都要遍历到最后,同时由于碎片化,大对象的分配效率会更慢PS:标记清除算法的缺点补充
归根结底,标记清除算法的缺点在于清除以后剩余的对象位置不变而致使的空闲内存不连续,因此只要解决这一点,两个缺点均可以完美解决了
而 标记整理(Mark-Compact)算法 就能够有效地解决,它的标记阶段和标记清除算法没有什么不一样,只是标记结束后,标记整理算法会将活着的对象(即不须要清理的对象)向内存的一端移动,最后清理掉边界的内存(以下图)
策略
引用计数(Reference Counting),这实际上是早先的一种垃圾回收算法,它把 对象是否再也不须要
简化定义为 对象有没有其余对象引用到它
,若是没有引用指向该对象(零引用),对象将被垃圾回收机制回收,目前不多使用这种算法了,由于它的问题不少,不过咱们仍是须要了解一下
它的策略是跟踪记录每一个变量值被使用的次数
当声明了一个变量而且将一个引用类型赋值给该变量的时候这个值的引用次数就为 1
若是同一个值又被赋给另外一个变量,那么引用数加 1
若是该变量的值被其余的值覆盖了,则引用次数减 1
当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值无法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存
以下例
let a = new Object() // 此对象的引用计数为 1(a引用)
let b = a // 此对象的引用计数是 2(a,b引用)
a = null // 此对象的引用计数为 1(b引用)
b = null // 此对象的引用计数为 0(无引用)
... // GC 回收此对象
复制代码
这种方式是否是很简单?确实很简单,不过在引用计数这种算法出现没多久,就遇到了一个很严重的问题——循环引用,即对象 A 有一个指针指向对象 B,而对象 B 也引用了对象 A ,以下面这个例子
function test(){
let A = new Object()
let B = new Object()
A.b = B
B.a = A
}
复制代码
如上所示,对象 A 和 B 经过各自的属性相互引用着,按照上文的引用计数策略,它们的引用数量都是 2,可是,在函数 test
执行完成以后,对象 A 和 B 是要被清理的,但使用引用计数则不会被清理,由于它们的引用数量不会变成 0,假如此函数在程序中被屡次调用,那么就会形成大量的内存不会被释放
咱们再用标记清除的角度看一下,当函数结束后,两个对象都不在做用域中,A 和 B 都会被看成非活动对象来清除掉,相比之下,引用计数则不会释放,也就会形成大量无用内存占用,这也是后来放弃引用计数,使用标记清除的缘由之一
在 IE8 以及更早版本的 IE 中,
BOM
和DOM
对象并不是是原生JavaScript
对象,它是由C++
实现的组件对象模型对象(COM,Component Object Model)
,而COM
对象使用 引用计数算法来实现垃圾回收,因此即便浏览器使用的是标记清除算法,只要涉及到COM
对象的循环引用,就仍是没法被回收掉,就好比两个互相引用的DOM
对象等等,而想要解决循环引用,须要将引用地址置为null
来切断变量与以前引用值的关系,以下// COM对象 let ele = document.getElementById("xxx") let obj = new Object() // 形成循环引用 obj.ele = ele ele.obj = obj // 切断引用关系 obj.ele = null ele.obj = null 复制代码
不过在 IE9 及之后的
BOM
与DOM
对象都改为了JavaScript
对象,也就避免了上面的问题此处参考 JavaScript高级程序设计 第四版 4.3.2 小节
优势
引用计数算法的优势咱们对比标记清除来看就会清晰不少,首先引用计数在引用值为 0 时,也就是在变成垃圾的那一刻就会被回收,因此它能够当即回收垃圾
而标记清除算法须要每隔一段时间进行一次,那在应用程序(JS脚本)运行过程当中线程就必需要暂停去执行一段时间的 GC
,另外,标记清除算法须要遍历堆里的活动以及非活动对象来清除,而引用计数则只须要在引用时计数就能够了
缺点
引用计数的缺点想必你们也都很明朗了,首先它须要一个计数器,而此计数器须要占很大的位置,由于咱们也不知道被引用数量的上限,还有就是没法解决循环引用没法回收的问题,这也是最严重的
咱们在上面也说过,如今大多数浏览器都是基于标记清除算法,V8 亦是,固然 V8 确定也对其进行了一些优化加工处理,那接下来咱们主要就来看 V8 中对垃圾回收机制的优化
试想一下,咱们上面所说的垃圾清理算法在每次垃圾回收时都要检查内存中全部的对象,这样的话对于一些大、老、存活时间长的对象来讲同新、小、存活时间短的对象一个频率的检查很很差,由于前者须要时间长而且不须要频繁进行清理,后者刚好相反,怎么优化这点呢???分代式就来了
V8 的垃圾回收策略主要基于分代式垃圾回收机制,V8 中将堆内存分为新生代和老生代两区域,采用不一样的垃圾回收器也就是不一样的策略管理垃圾回收
新生代的对象为存活时间较短的对象,简单来讲就是新产生的对象,一般只支持 1~8M
的容量,而老生代的对象为存活事件较长或常驻内存的对象,简单来讲就是经历过新生代垃圾回收后还存活下来的对象,容量一般比较大
V8 整个堆内存的大小就等于新生代加上老生代的内存(以下图)
对于新老两块内存区域的垃圾回收,V8 采用了两个垃圾回收器来管控,咱们暂且将管理新生代的垃圾回收器叫作新生代垃圾回收器,一样的,咱们称管理老生代的垃圾回收器叫作老生代垃圾回收器好了
新生代对象是经过一个名为 Scavenge
的算法进行垃圾回收,在 Scavenge算法
的具体实现中,主要采用了一种复制式的方法即 Cheney算法
,咱们细细道来
Cheney算法
中将堆内存一分为二,一个是处于使用状态的空间咱们暂且称之为 使用区
,一个是处于闲置状态的空间咱们称之为 空闲区
,以下图所示
新加入的对象都会存放到使用区,当使用区快被写满时,就须要执行一次垃圾清理操做
当开始进行垃圾回收时,新生代垃圾回收器会对使用区中的活动对象作标记,标记完成以后将使用区的活动对象复制进空闲区并进行排序,随后进入垃圾清理阶段,即将非活动对象占用的空间清理掉。最后进行角色互换,把原来的使用区变成空闲区,把原来的空闲区变成使用区
当一个对象通过屡次复制后依然存活,它将会被认为是生命周期较长的对象,随后会被移动到老生代中,采用老生代的垃圾回收策略进行管理
另外还有一种状况,若是复制一个对象到空闲区时,空闲区空间占用超过了 25%,那么这个对象会被直接晋升到老生代空间中,设置为 25% 的比例的缘由是,当完成 Scavenge
回收后,空闲区将翻转成使用区,继续进行对象内存的分配,若占比过大,将会影响后续内存分配
相比于新生代,老生代的垃圾回收就比较容易理解了,上面咱们说过,对于大多数占用空间大、存活时间长的对象会被分配到老生代里,由于老生代中的对象一般比较大,若是再如新生代通常分区而后复制来复制去就会很是耗时,从而致使回收执行效率不高,因此老生代垃圾回收器来管理其垃圾回收执行,它的整个流程就采用的就是上文所说的标记清除算法了
首先是标记阶段,从一组根元素开始,递归遍历这组根元素,遍历过程当中能到达的元素称为活动对象,没有到达的元素就能够判断为非活动对象
清除阶段老生代垃圾回收器会直接将非活动对象,也就是数据清理掉
前面咱们也提过,标记清除算法在清除后会产生大量不连续的内存碎片,过多的碎片会致使大对象没法分配到足够的连续内存,而 V8 中就采用了咱们上文中说的标记整理算法来解决这一问题来优化空间
正如小标题,为何须要分代式?这个机制有什么优势又解决了什么问题呢?
其实,它并不能说是解决了什么问题,能够说是一个优化点吧
分代式机制把一些新、小、存活时间短的对象做为新生代,采用一小块内存频率较高的快速清理,而一些大、老、存活时间长的对象做为老生代,使其不多接受检查,新老生代的回收机制及频率是不一样的,能够说此机制的出现很大程度提升了垃圾回收机制的效率
在介绍并行以前,咱们先要了解一个概念 全停顿(Stop-The-World)
,咱们都知道 JavaScript
是一门单线程的语言,它是运行在主线程上的,那在进行垃圾回收时就会阻塞 JavaScript
脚本的执行,需等待垃圾回收完毕后再恢复脚本执行,咱们把这种行为叫作 全停顿
好比一次 GC
须要 60ms
,那咱们的应用逻辑就得暂停 60ms
,假如一次 GC
的时间过长,对用户来讲就可能形成页面卡顿等问题
既然存在执行一次 GC
比较耗时的状况,考虑到一我的盖房子难,那两我的、十我的...呢?切换到程序这边,那咱们可不能够引入多个辅助线程来同时处理,这样是否是就会加速垃圾回收的执行速度呢?所以 V8 团队引入了并行回收机制
所谓并行,也就是同时的意思,它指的是垃圾回收器在主线程上执行的过程当中,开启多个辅助线程,同时执行一样的回收工做
简单来讲,使用并行回收,假如原本是主线程一我的干活,它一我的须要 3 秒,如今叫上了 2 个辅助线程和主线程一块干活,那三我的一块干一我的干 1 秒就完事了,可是因为多人协同办公,因此须要加上一部分多人协同(同步开销)的时间咱们算 0.5 秒好了,也就是说,采用并行策略后,原本要 3 秒的活如今 1.5 秒就能够干完了
不过虽然 1.5 秒就能够干完了,时间也大大缩小了,可是这 1.5 秒内,主线程仍是须要让出来的,也正是由于主线程仍是须要让出来,这个过程内存是静态的,不须要考虑内存中对象的引用关系改变,只须要考虑协同,实现起来也很简单
新生代对象空间就采用并行策略,在执行垃圾回收的过程当中,会启动了多个线程来负责新生代中的垃圾清理操做,这些线程同时将对象空间中的数据移动到空闲区域,这个过程当中因为数据地址会发生改变,因此还须要同步更新引用这些对象的指针,此即并行回收
咱们上面所说的并行策略虽然能够增长垃圾回收的效率,对于新生代垃圾回收器可以有很好的优化,可是其实它仍是一种全停顿式的垃圾回收方式,对于老生代来讲,它的内部存放的都是一些比较大的对象,对于这些大的对象 GC
时哪怕咱们使用并行策略依然可能会消耗大量时间
因此为了减小全停顿的时间,在 2011 年,V8 对老生代的标记进行了优化,从全停顿标记切换到增量标记
增量就是将一次 GC
标记的过程,分红了不少小步,每执行完一小步就让应用逻辑执行一下子,这样交替屡次后完成一轮 GC
标记(以下图)
试想一下,将一次完整的 GC
标记分次执行,那在每一小次 GC
标记执行完以后如何暂停下来去执行任务程序,然后又怎么恢复呢?那假如咱们在一次完整的 GC
标记分块暂停后,执行任务程序时内存中标记好的对象引用关系被修改了又怎么办呢?
能够看出增量的实现要比并行复杂一点,V8 对这两个问题对应的解决方案分别是三色标记法与写屏障
咱们知道老生代是采用标记清理算法,而上文的标记清理中咱们说过,也就是在没有采用增量算法以前,单纯使用黑色和白色来标记数据就能够了,其标记流程即在执行一次完整的 GC
标记前,垃圾回收器会将全部的数据置为白色,而后垃圾回收器在会从一组跟对象出发,将全部能访问到的数据标记为黑色,遍历结束以后,标记为黑色的数据对象就是活动对象,剩余的白色数据对象也就是待清理的垃圾对象
若是采用非黑即白的标记策略,那在垃圾回收器执行了一段增量回收后,暂停后启用主线程去执行了应用程序中的一段 JavaScript
代码,随后当垃圾回收器再次被启动,这时候内存中黑白色都有,咱们没法得知下一步走到哪里了
为了解决这个问题,V8 团队采用了一种特殊方式: 三色标记法
三色标记法即便用每一个对象的两个标记位和一个标记工做表来实现标记,两个标记位编码三种颜色:白、灰、黑
如上图所示,咱们用最简单的表达方式来解释这一过程,最初全部的对象都是白色,意味着回收器没有标记它们,从一组根对象开始,先将这组根对象标记为灰色并推入到标记工做表中,当回收器从标记工做表中弹出对象并访问它的引用对象时,将其自身由灰色转变成黑色,并将自身的下一个引用对象转为灰色
就这样一直往下走,直到没有可标记灰色的对象时,也就是无可达(无引用到)的对象了,那么剩下的全部白色对象都是没法到达的,即等待回收(如上图中的 C、E
将要等待回收)
采用三色标记法后咱们在恢复执行时就好办多了,能够直接经过当前内存中有没有灰色节点来判断整个标记是否完成,如没有灰色节点,直接进入清理阶段,如还有灰色标记,恢复时直接从灰色的节点开始继续执行就能够
三色标记法的 mark 操做能够渐进执行的而不需每次都扫描整个内存空间,能够很好的配合增量回收进行暂停恢复的一些操做,从而减小 全停顿
的时间
一次完整的 GC
标记分块暂停后,执行任务程序时内存中标记好的对象引用关系被修改了,增量中修改引用,可能不太好理解,咱们举个例子(如图)
假如咱们有 A、B、C
三个对象依次引用,在第一次增量分段中所有标记为黑色(活动对象),然后暂停开始执行应用程序也就是 JavaScript 脚本,在脚本中咱们将对象 B
的指向由对象 C
改成了对象 D
,接着恢复执行下一次增量分段
这时其实对象 C
已经无引用关系了,可是目前它是黑色(表明活动对象)此一整轮 GC
是不会清理 C
的,不过咱们能够不考虑这个,由于就算此轮不清理等下一轮 GC
也会清理,这对咱们程序运行并无太大影响
咱们再看新的对象 D
是初始的白色,按照咱们上面所说,已经没有灰色对象了,也就是所有标记完毕接下来要进行清理了,新修改的白色对象 D
将在次轮 GC
的清理阶段被回收,还有引用关系就被回收,后面咱们程序里可能还会用到对象 D
呢,这确定是不对的
为了解决这个问题,V8 增量回收使用 写屏障 (Write-barrier)
机制,即一旦有黑色对象引用白色对象,该机制会强制将引用的白色对象改成灰色,从而保证下一次增量 GC
标记阶段能够正确标记,这个机制也被称做 强三色不变性
那在咱们上图的例子中,将对象 B
的指向由对象 C
改成对象 D
后,白色对象 D
会被强制改成灰色
增量标记其实只是对活动对象和非活动对象进行标记,对于真正的清理释放内存 V8 采用的是惰性清理(Lazy Sweeping)
增量标记完成后,惰性清理就开始了。当增量标记完成后,假如当前的可用内存足以让咱们快速的执行代码,其实咱们是不必当即清理内存的,能够将清理过程稍微延迟一下,让 JavaScript
脚本代码先执行,也无需一次性清理完全部非活动对象内存,能够按需逐一进行清理直到全部的非活动对象内存都清理完毕,后面再接着执行增量标记
增量标记与惰性清理的出现,使得主线程的停顿时间大大减小了,让用户与浏览器交互的过程变得更加流畅。可是因为每一个小的增量标记之间执行了 JavaScript
代码,堆中的对象指针可能发生了变化,须要使用写屏障技术来记录这些引用关系的变化,因此增量标记缺点也很明显:
首先是并无减小主线程的总暂停的时间,甚至会略微增长,其次因为写屏障机制的成本,增量标记可能会下降应用程序的吞吐量(吞吐量是啥总不用说了吧)
前面咱们说并行回收依然会阻塞主线程,增量标记一样有增长了总暂停时间、下降应用程序吞吐量两个缺点,那么怎么才能在不阻塞主线程的状况下执行垃圾回收而且与增量相比更高效呢?
这就要说到并发回收了,它指的是主线程在执行 JavaScript
的过程当中,辅助线程可以在后台完成执行垃圾回收的操做,辅助线程在执行垃圾回收的时候,主线程也能够自由执行而不会被挂起(以下图)
辅助线程在执行垃圾回收的时候,主线程也能够自由执行而不会被挂起,这是并发的优势,但一样也是并发回收实现的难点,由于它须要考虑主线程在执行 JavaScript
时,堆中的对象引用关系随时都有可能发生变化,这时辅助线程以前作的一些标记或者正在进行的标记就会要有所改变,因此它须要额外实现一些读写锁机制来控制这一点,这里咱们再也不细说
V8 的垃圾回收策略主要基于分代式垃圾回收机制,这咱们说过,关于新生代垃圾回收器,咱们说使用并行回收能够很好的增长垃圾回收的效率,那老生代垃圾回收器用的哪一个策略呢?我上面说了并行回收、增量标记与惰性清理、并发回收这几种回收方式来提升效率、优化体验,看着一个比一个好,那老生代垃圾回收器到底用的哪一个策略?难道是并发??心里独白:” 好像。。貌似。。并发回收效率最高 “
其实,这三种方式各有优缺点,因此在老生代垃圾回收器中这几种策略都是融合使用的
老生代主要使用并发标记,主线程在开始执行 JavaScript
时,辅助线程也同时执行标记操做(标记操做全都由辅助线程完成)
标记完成以后,再执行并行清理操做(主线程在执行清理操做时,多个辅助线程也同时执行清理操做)
同时,清理的任务会采用增量的方式分批在各个 JavaScript
任务之间执行
那上面就是 V8 引擎为咱们的垃圾回收所作的一些主要优化了,虽然引擎有优化,但并非说咱们就能够彻底不用关心垃圾回收这块了,咱们的代码中依然要主动避免一些不利于引擎作垃圾回收操做,由于不是全部无用对象内存均可以被回收的,那当再也不用到的内存,没有及时回收时,咱们叫它 内存泄漏
关于内存泄漏又是另外一个点了,也碍于篇幅就不放在这篇文章了
收工,看也看完了,开头的问题你有更深层次的答案了吗?在以前面试时我问过面试者这类问题,大多同窗的回答都仅限于标记清除+引用计数两个概念,往深处各类缺陷以及优化上挖一挖就说不出了,其实咱们结合 V8 引擎对垃圾回收的优化来回答上面那些问题会更好一些,那么,评论区码出本身的理解吧!
另外,有哪些没有 Get 到的点能够评论留言,也欢迎指错勘误!!!