请选择 进入手机版 | 继续访问电脑版
查看: 461|回复: 13

V8引擎的垃圾回收

  [复制链接]
  • TA的每日心情
    开心
    6 天前
  • 签到天数: 1608 天

    [LV.Master]伴坛终老

    4251

    主题

    6175

    帖子

    11万

    积分

    管理员

    IBC编程社区-原道楠

    Rank: 9Rank: 9Rank: 9

    积分
    111214

    推广达人突出贡献优秀版主荣誉管理论坛元老

    发表于 2019-12-25 08:59:13 | 显示全部楼层 |阅读模式

    马上加入IBC,查看更多教程

    您需要 登录 才可以下载或查看,没有帐号?立即注册

    x
    弁言

    作为现在最盛行的JavaScript引擎,V8引擎从出现的那一刻起便广泛受到人们的关注,我们知道,JavaScript可以高效地运行在欣赏器和Nodejs这两大宿主情况中,也是由于背后有强大的V8引擎在为其保驾护航,乃至成绩了Chrome在欣赏器中的霸主职位。不得不说,V8引擎为了寻求极致的性能和更好的用户体验,为我们做了太多太多,从原始的Full-codegen和Crankshaft编译器升级为Ignition表明器和TurboFan编译器的强强组合,到埋伏类,内联缓存和HotSpot热门代码网络等一系列强有力的优化计谋,V8引擎正在积极低沉团体的内存占用和提升到更高的运行性能。
    本篇重要是从V8引擎的垃圾接纳机制入手,解说一下在JavaScript代码实验的整个生命周期中V8引擎是接纳怎样的垃圾接纳计谋来减少内存占比的,固然这部门的知识并不太影响我们写代码的流程,毕竟在一样平常情况下我们很少会碰到欣赏器端出现内存溢出而导致步伐瓦解的情况,但是至少我们对这方面有肯定的相识之后,能加强我们在写代码过程中对减少内存占用,克制内存走漏的主观意识,大概可以或许资助你写出更加结实和对V8引擎更加友好的代码。本文也是笔者在查阅资料巩固复习的过程中逐步总结和整理出来的,若文中有错误的地方,还请指正。
    1、为何必要垃圾接纳

    我们知道,在V8引擎逐行实验JavaScript代码的过程中,当碰到函数的情况时,会为其创建一个函数实验上下文(Context)情况并添加到调用堆栈的栈顶,函数的作用域(handleScope)中包罗了该函数中声明的全部变量,当该函数实验完毕后,对应的实验上下文从栈顶弹出,函数的作用域会随之销毁,其包罗的全部变量也会同一开释并被自动接纳。试想如果在这个作用域被销毁的过程中,此中的变量不被接纳,即恒久占用内存,那么肯定会导致内存暴增,从而引发内存走漏导致步伐的性能直线降落乃至瓦解,因此内存在利用完毕之后理当归还给操纵系统以包管内存的重复利用。
    这个过程就好比你向亲戚朋侪乞贷,借得多告终不按时归还,那么你再下次乞贷的时间肯定没有那么顺遂了,大概说你的亲戚朋侪不乐意再借你了,导致你的手头有点儿紧(内存走漏,性能降落),以是说有借有还,再借不难嘛,毕竟出来混都是要还的。
    但是JavaScript作为一门高级编程语言,并不像C语言或C++语言中须要手动地申请分配和开释内存,V8引擎已经帮我们自动举行了内存的分配和管理,好让我们有更多的精力去专注于业务层面的复杂逻辑,这对于我们前端开发职员来说是一项福利,但是随之带来的标题也是显而易见的,那就是由于不消去手动管理内存,导致写代码的过程中不敷严谨从而容易引发内存走漏(毕竟这是别人对你的好,你没有付出过,又怎能相识得到?)。
    2、V8引擎的内存限定

    固然V8引擎资助我们实现了自动的垃圾接纳管理,解放了我们勤劳的双手,但V8引擎中的内存利用也并不是无穷制的。详细来说,默认情况下,V8引擎在64位系统下最多只能利用约1.4GB的内存,在32位系统下最多只能利用约0.7GB的内存,在如许的限定下,肯定会导致在node中无法直接操纵大内存对象,好比将一个2GB巨细的文件全部读入内存举行字符串分析处置处罚,纵然物理内存高达32GB也无法充实利用盘算机的内存资源,那么为什么会有这种限定呢?这个要回到V8引擎的计划之初,早先只是作为欣赏器端JavaScript的实验情况,在欣赏器端我们实在很少会碰到利用大量内存的场景,因此也就没有须要将最大内存设置得过高。但这只是一方面,实在另有别的两个重要的缘故起因:
    • JS单线程机制:作为欣赏器的脚本语言,JS的重要用途是与用户交互以及操纵DOM,那么这也决定了其作为单线程的本质,单线程意味着实验的代码必须按序次实验,在同一时间只能处置处罚一个任务。试想如果JS是多线程的,一个线程在删除DOM元素的同时,另一个线程对该元素举行修改操纵,那么肯定会导致复杂的同步标题。既然JS是单线程的,那么也就意味着在V8实验垃圾接纳时,步伐中的其他各种逻辑都要进入停息等待阶段,直到垃圾接纳竣事后才会再次重新实验JS逻辑。因此,由于JS的单线程机制,垃圾接纳的过程拦阻了主线程逻辑的实验。
      固然JS是单线程的,但是为了可以或许充实利用操纵系统的多核CPU盘算本领,在HTML5中引入了新的Web Worker尺度,其作用就是为JS创造多线程情况,答应主线程创建Worker线程,将一些任务分配给后者运行。在主线程运行的同时,Worker在配景运行,两者互不干扰。等到Worker线程完成盘算任务,再把效果返回给主线程。如许的利益是, 一些盘算麋集型或高耽误的任务,被Worker线程负担,主线程(通常负责UI交互)就会很流通,不会被壅闭大概拖慢。Web Worker不是JS的一部门,而是通过JS访问的欣赏器特性,其固然创造了一个多线程的实验情况,但是子线程完全受主线程控制,不能访问欣赏器特定的API,比方操纵DOM,因此这个新尺度并没有改变JS单线程的本质。
    • 垃圾接纳机制:垃圾接纳自己也是一件非常耗时的操纵,假设V8的堆内存为1.5G,那么V8做一次小的垃圾接纳须要50ms以上,而做一次非增量式接纳乃至须要1s以上,可见其耗时之久,而在这1s的时间内,欣赏器不停处于等待的状态,同时会失去对用户的相应,如果有动画正在运行,也会造成动画卡顿掉帧的情况,严肃影相应用步伐的性能。因此如果内存利用过高,那么肯定会导致垃圾接纳的过程痴钝,也就会导致主线程的等待时间越长,欣赏器也就越长时间得不到相应。
    基于以上两点,V8引擎为了减少对应用的性能造成的影响,接纳了一种比力粗暴的本领,那就是直接限定堆内存的巨细,毕竟在欣赏器端一样平常也不会碰到须要操纵几个G内存如许的场景。但是在node端,涉及到的I/O操纵大概会比欣赏器端更加复杂多样,因此更有大概出现内存溢出的情况。不外也没关系,V8为我们提供了可设置项来让我们手动地调解内存巨细,但是须要在node初始化的时间举行设置,我们可以通过如下方式来手动设置。
    我们实验在node下令行中输入以下下令:
    笔者本地安装的node版本为v10.14.2,可通过node -v检察本地node的版本号,差别版本大概会导致下面的下令会有所差别。
    1. // 该下令可以用来检察node中可用的V8引擎的选项及其寄义node --v8-options
    复制代码
    然后我们会在下令行窗口中看到大量关于V8的选项,这里我们暂时只关注图中赤色选框中的几个选项:
    090313vweha9a5jzhlz9d9.jpg

    1. // 设置新生代内存中单个半空间的内存最小值,单位MBnode --min-semi-space-size=1024 xxx.js// 设置新生代内存中单个半空间的内存最大值,单位MBnode --max-semi-space-size=1024 xxx.js// 设置老生代内存最大值,单位MBnode --max-old-space-size=2048 xxx.js
    复制代码
    通过以上方法便可以手动放宽V8引擎所利用的内存限定,同时node也为我们提供了process.memoryUsage()方法来让我们可以检察当前node进程所占用的现实内存巨细。
    090314ep8bdibldbbd8rpd.jpg

    在上图中,包罗的几个字段的寄义分别如下所示,单位均为字节:
    • heapTotal:表现V8当前申请到的堆内存总巨细。
    • heapUsed:表现当前内存利用量。
    • external:表现V8内部的C++对象所占用的内存。
    • rss(resident set size):表现驻留集巨细,是给这个node进程分配了多少物理内存,这些物理内存中包罗堆,栈和代码片断。对象,闭包等存于堆内存,变量存于栈内存,现实的JavaScript源代码存于代码段内存。利用Worker线程时,rss将会是一个对整个进程有效的值,而其他字段则只针对当火线程。
    在JS中声明对象时,该对象的内存就分配在堆中,如果当前已申请的堆内存已经不敷分配新的对象,则会继续申请堆内存直到堆的巨细高出V8的限定为止。
    3、V8的垃圾接纳计谋

    V8的垃圾接纳计谋重要是基于分代式垃圾接纳机制,其根据对象的存活时间将内存的垃圾接纳举行差别的分代,然后对差别的分代接纳差别的垃圾接纳算法。
    3.1 V8的内存结构

    在V8引擎的堆结构构成中,实在除了新生代和老生代外,还包罗其他几个部门,但是垃圾接纳的过程重要出现在新生代和老生代,以是对于其他的部门我们没须要做太多的深入,有爱好的小同伴儿可以查阅下干系资料,V8的内存结构重要由以下几个部门构成:
    • 新生代(new_space):大多数的对象开始都会被分配在这里,这个地域相对较小但是垃圾接纳特殊频仍,该地域被分为两半,一半用来分配内存,另一半用于在垃圾接纳时将须要保存的对象复制过来。
    • 老生代(old_space):新生代中的对象在存活一段时间后就会被转移到老生代内存区,相对于新生代该内存地域的垃圾接纳频率较低。老生代又分为老生代指针区和老生代数据区,前者包罗大多数大概存在指向其他对象的指针的对象,后者只生存原始数据对象,这些对象没有指向其他对象的指针。
    • 大对象区(large_object_space):存放体积逾越其他地域巨细的对象,每个对象都会有自己的内存,垃圾接纳不会移动大对象区。
    • 代码区(code_space):代码对象,会被分配在这里,唯一拥有实验权限的内存地域。
    • map区(map_space):存放Cell和Map,每个地域都是存放类似巨细的元素,结构简朴(这里没有做详细深入的相识,有清晰的小同伴儿还贫苦表明下)。
    内存结构图如下所示:
    090314z1n12satcslu3tno.jpg

    上图中的带斜纹的地域代表暂未利用的内存,新生代(new_space)被分别为了两个部门,此中一部门叫做inactive new space,表现暂未激活的内存地域,另一部门为激活状态,为什么会分别为两个部门呢,在下一末节我们会讲到。
    3.2 新生代

    在V8引擎的内存结构中,新生代重要用于存放存活时间较短的对象。新生代内存是由两个semispace(半空间)构成的,内存最大值在64位系统和32位系统上分别为32MB和16MB,在新生代的垃圾接纳过程中重要接纳了Scavenge算法。
    Scavenge算法是一种典范的捐躯空间变动时间的算法,对于老生代内存来说,大概会存储大量对象,如果在老生代中利用这种算法,势必会造成内存资源的浪费,但是在新生代内存中,大部门对象的生命周期较短,在时间服从上表现可观,以是照旧比力适合这种算法。
    在Scavenge算法的详细实现中,重要接纳了Cheney算法,它将新生代内存一分为二,每一个部门的空间称为semispace,也就是我们在上图中看见的new_space中分别的两个地域,此中处于激活状态的地域我们称为From空间,未激活(inactive new space)的地域我们称为To空间。这两个空间中,始终只有一个处于利用状态,另一个处于闲置状态。我们的步伐中声明的对象起首会被分配到From空间,当举行垃圾接纳时,如果From空间中尚有存活对象,则会被复制到To空间举行生存,非存活的对象会被自动接纳。当复制完成后,From空间和To空间完成一次脚色互换,To空间会变为新的From空间,原来的From空间则变为To空间。
    基于以上算法,我们可以画出如下的流程图:
    • 假设我们在From空间中分配了三个对象A、B、C
    090315prxf7ozorir2pjyv.jpg

    • 当步伐主线程任务第一次实验完毕后进入垃圾接纳时,发现对象A已经没有其他引用,则表现可以对其举行接纳
    090315qp071xux30j7rusn.jpg

    • 对象B和对象C此时仍旧处于生动状态,因此会被复制到To空间中举行生存
    090315e5g01b90vlolp15v.jpg

    • 接下来将From空间中的全部非存活对象全部扫除
    090315xjoemi4j74ok4oa6.jpg

    • 此时From空间中的内存已经清空,开始和To空间完成一次脚色互换
    090316ssf8z3s1jwig8ilg.jpg

    • 当步伐主线程在实验第二个任务时,在From空间中分配了一个新对象D
    090316jamg8sku8gbak2q6.jpg

    • 任务实验完毕后再次进入垃圾接纳,发现对象D已经没有其他引用,表现可以对其举行接纳
    090316y3pthlhpatf0f2kt.jpg

    • 对象B和对象C此时仍旧处于生动状态,再次被复制到To空间中举行生存
    090316bj4xill7zp4jcc37.jpg

    • 再次将From空间中的全部非存活对象全部扫除
    090316m1m11j2jpwoc8yri.jpg

    • From空间和To空间继续完成一次脚色互换
    090317zc8dd0p0ud7mm06c.jpg

    通过以上的流程图,我们可以很清晰地看到,Scavenge算法的垃圾接纳过程重要就是将存活对象在From空间和To空间之间举行复制,同时完成两个空间之间的脚色互换,因此该算法的缺点也比力显着,浪费了一半的内存用于复制。
    3.3 对象提升

    当一个对象在颠末多次复制之后仍旧存活,那么它会被以为是一个生命周期较长的对象,在下一次举行垃圾接纳时,该对象会被直接转移到老生代中,这种对象重新生代转移到老生代的过程我们称之为提升。
    对象提升的条件重要有以下两个:
    • 对象是否履历过一次Scavenge算法
    • To空间的内存占比是否已经高出25%
    默认情况下,我们创建的对象都会分配在From空间中,当举行垃圾接纳时,在将对象从From空间复制到To空间之前,会先查抄该对象的内存地点来判断是否已经履历过一次Scavenge算法,如果地点已经发生变动则会将该对象转移到老生代中,不会再被复制到To空间,可以用以下的流程图来表现:
    090317iv63hw3b3z67t6bc.jpg

    如果对象没有履历过Scavenge算法,会被复制到To空间,但是如果此时To空间的内存占比已经高出25%,则该对象仍旧会被转移到老生代,如下图所示:
    090317ela6iwsyyajpw5zy.jpg

    之以是有25%的内存限定是由于To空间在履历过一次Scavenge算法后会和From空间完成脚色互换,会变为From空间,后续的内存分配都是在From空间中举行的,如果内存利用过高乃至溢出,则会影响后续对象的分配,因此高出这个限定之后对象会被直接转移到老生代来举行管理。
    3.4 老生代

    在老生代中,由于管理着大量的存活对象,如果仍旧利用Scavenge算法的话,很显着会浪费一半的内存,因此已经不再利用Scavenge算法,而是接纳新的算法Mark-Sweep(标记扫除)和Mark-Compact(标记整理)来举行管理。
    在早前我们大概听说过一种算法叫做引用计数,该算法的原理比力简朴,就是看对象是否另有其他引用指向它,如果没有指向该对象的引用,则该对象会被视为垃圾并被垃圾接纳器接纳,示比方下:
    1. // 创建了两个对象obj1和obj2,此中obj2作为obj1的属性被obj1引用,因此不会被垃圾接纳let obj1 = {    obj2: {        a: 1    }}// 创建obj3并将obj1赋值给obj3,让两个对象指向同一个内存地点let obj3 = obj1;// 将obj1重新赋值,此时原来obj1指向的对象现在只由obj3来表现obj1 = null;// 创建obj4并将obj3.obj2赋值给obj4// 此时obj2所指向的对象有两个引用:一个是作为obj3的属性,另一个是变量obj4let obj4 = obj3.obj2;// 将obj3重新赋值,此时本可以对obj3指向的对象举行接纳,但是由于obj3.obj2被obj4所引用,因此仍旧不能被接纳obj3 = null;// 此时obj3.obj2已经没有指向它的引用,因此obj3指向的对象在此时可以被接纳obj4 = null;
    复制代码
    上述例子在颠末一系列操纵后终极对象会被垃圾接纳,但是一旦我们碰到循环引用的场景,就会出现标题,我们看下面的例子:
    1. function foo() {    let a = {};    let b = {};    a.a1 = b;    b.b1 = a;}foo();
    复制代码
    这个例子中我们将对象a的a1属性指向对象b,将对象b的b1属性指向对象a,形成两个对象相互引用,在foo函数实验完毕后,函数的作用域已经被销毁,作用域中包罗的变量a和b本应该可以被接纳,但是由于接纳了引用计数的算法,两个变量均存在指向自身的引用,因此仍旧无法被接纳,导致内存走漏。
    因此为了克制循环引用导致的内存走漏标题,停止2012年全部的当代欣赏器均放弃了这种算法,转而接纳新的Mark-Sweep(标记扫除)和Mark-Compact(标记整理)算法。在上面循环引用的例子中,由于变量a和变量b无法从window全局对象访问到,因此无法对其举行标记,以是终极会被接纳。
    Mark-Sweep(标记扫除)分为标记和扫除两个阶段,在标记阶段会遍历堆中的全部对象,然后标记在世的对象,在扫除阶段中,会将殒命的对象举行扫除。Mark-Sweep算法重要是通过判断某个对象是否可以被访问到,从而知道该对象是否应该被接纳,详细步调如下:
    • 垃圾接纳器会在内部构建一个根列表,用于从根节点出发去探求那些可以被访问到的变量。好比在JavaScript中,window全局对象可以看成一个根节点。
    • 然后,垃圾接纳器从全部根节点出发,遍历其可以访问到的子节点,并将其标记为活动的,根节点不能到达的地方即为非活动的,将会被视为垃圾。
    • 末了,垃圾接纳器将会开释全部非活动的内存块,并将其归还给操纵系统。
    以下几种情况都可以作为根节点:
    • 全局对象
    • 本地函数的局部变量和参数
    • 当前嵌套调用链上的其他函数的变量和参数
    090317kg64wfvn6wuavz1f.jpg

    但是Mark-Sweep算法存在一个标题,就是在履历过一次标记扫除后,内存空间大概会出现不连续的状态,由于我们所清理的对象的内存地点大概不是连续的,以是就会出现内存碎片的标题,导致反面如果须要分配一个大对象而空闲内存不敷以分配,就会提前触发垃圾接纳,而这次垃圾接纳实在是没须要的,由于我们确实有许多空闲内存,只不外是不连续的。
    为了管理这种内存碎片的标题,Mark-Compact(标记整理)算法被提了出来,该算法重要就是用来管理内存的碎片化标题标,接纳过程中将殒命对象扫除后,在整理的过程中,会将活动的对象往堆内存的一端举行移动,移动完成后再清理掉界限外的全部内存,我们可以用如下游程图来表现:
    • 假设在老生代中有A、B、C、D四个对象
    090318sj4a2lg49qzggg4j.jpg

    • 在垃圾接纳的标记阶段,将对象A和对象C标记为活动的
    090318nadatqpqdvkjz8nn.jpg

    • 在垃圾接纳的整理阶段,将活动的对象往堆内存的一端移动
    090318mj7jvv62y5j1y5zy.jpg

    • 在垃圾接纳的扫除阶段,将活动对象左侧的内存全部接纳
    090318p0ioosm99mzizzyk.jpg

    至此就完成了一次老生代垃圾接纳的全部过程,我们在前文中说过,由于JS的单线程机制,垃圾接纳的过程会拦阻主线程同步任务的实验,待实验完垃圾接纳后才会再次规复实验主任务的逻辑,这种活动被称为全停顿(stop-the-world)。在标记阶段同样会拦阻主线程的实验,一样平常来说,老生代会生存大量存活的对象,如果在标记阶段将整个堆内存遍历一遍,那么势必会造成严肃的卡顿。
    因此,为了减少垃圾接纳带来的停顿时间,V8引擎又引入了Incremental Marking(增量标记)的概念,即将本来须要一次性遍历堆内存的操纵改为增量标记的方式,先标记堆内存中的一部门对象,然后停息,将实验权重新交给JS主线程,待主线程任务实验完毕后再从原来停息标记的地方继续标记,直到标记完备个堆内存。这个理念实在有点像React框架中的Fiber架构,只有在欣赏器的空闲时间才会去遍历Fiber Tree实验对应的任务,否则耽误实验,尽大概少地影响主线程的任务,克制应用卡顿,提升应用性能。
    得益于增量标记的利益,V8引擎后续继续引入了耽误清理(lazy sweeping)和增量式整理(incremental compaction),让清理和整理的过程也酿成增量式的。同时为了充实利用多核CPU的性能,也将引入并行标记和并行清理,进一步地减少垃圾接纳对主线程的影响,为应用提升更多的性能。
    4、怎样克制内存走漏

    在我们写代码的过程中,根本上都不太会关注写出怎样的代码才能有效地克制内存走漏,大概说欣赏器和大部门的前端框架在底层已经资助我们处置处罚了常见的内存走漏标题,但是我们照旧有须要相识一下常见的几种克制内存走漏的方式,毕竟在面试过程中也是常常观察的要点。
    4.1 尽大概少地创建全局变量

    在ES5中以var声明的方式在全局作用域中创建一个变量时,大概在函数作用域中不以任何声明的方式创建一个变量时,都会无形地挂载到window全局对象上,如下所示:
    1. var a = 1; // 等价于 window.a = 1;
    复制代码
    1. function foo() {    a = 1;}
    复制代码
    等价于
    1. function foo() {    window.a = 1;}
    复制代码
    我们在foo函数中创建了一个变量a但是忘记利用var来声明,此时会心想不到地创建一个全局变量并挂载到window对象上,别的另有一种比力埋伏的方式来创建全局变量:
    1. function foo() {    this.a = 1;}foo(); // 相称于 window.foo()
    复制代码
    当foo函数在调用时,它所指向的运行上下文情况为window全局对象,因此函数中的this指向的实在是window,也就偶然创建了一个全局变量。当举行垃圾接纳时,在标记阶段由于window对象可以作为根节点,在window上挂载的属性均可以被访问到,并将其标记为活动的从而常驻内存,因此也就不会被垃圾接纳,只有在整个进程退出时全局作用域才会被销毁。如果你碰到须要必须利用全局变量的场景,那么请包管肯定要在全局变量利用完毕后将其设置为null从而触发接纳机制。
    4.2 手动扫除定时器

    在我们的应用中常常会有利用setTimeout大概setInterval等定时器的场景,定时器自己是一个非常有效的功能,但是如果我们稍不注意,忘记在恰当的时间手动扫除定时器,那么很有大概就会导致内存走漏,示比方下:
    1. const numbers = [];const foo = function() {    for(let i = 0;i < 100000;i++) {        numbers.push(i);    }};window.setInterval(foo, 1000);
    复制代码
    在这个示例中,由于我们没有手动扫除定时器,导致回调任务会不停地实验下去,回调中所引用的numbers变量也不会被垃圾接纳,终极导致numbers数组长度无穷递增,从而引发内存走漏。
    4.3 少用闭包

    闭包是JS中的一个高级特性,奇妙地利用闭包可以资助我们实现许多高级功能。一样平常来说,我们在查找变量时,在本地作用域中查找不到就会沿着作用域链从内向外单向查找,但是闭包的特性可以让我们在外部作用域访问内部作用域中的变量,示比方下:
    1. function foo() {    let local = 123;    return function() {        return local;    }}const bar = foo();console.log(bar()); // -> 123
    复制代码
    在这个示例中,foo函数实验完毕后会返回一个匿名函数,该函数内部引用了foo函数中的局部变量local,而且通过变量bar来引用这个匿名的函数定义,通过这种闭包的方式我们就可以在foo函数的外部作用域中访问到它的局部变量local。一样平常情况下,当foo函数实验完毕后,它的作用域会被销毁,但是由于存在变量引用其返回的匿名函数,导致作用域无法得到开释,也就导致local变量无法接纳,只有当我们取消掉对匿名函数的引用才会进入垃圾接纳阶段。
    4.4 扫除DOM引用

    以往我们在操纵DOM元素时,为了克制多次获取DOM元素,我们会将DOM元素存储在一个数据字典中,示比方下:
    1. const elements = {    button: document.getElementById(&#39;button&#39;)};function removeButton() {    document.body.removeChild(document.getElementById(&#39;button&#39;));}
    复制代码
    在这个示例中,我们想调用removeButton方法来扫除button元素,但是由于在elements字典中存在对button元素的引用,以是纵然我们通过removeChild方法移除了button元素,它实在照旧仍旧存储在内存中无法得到开释,只有我们手动扫除对button元素的引用才会被垃圾接纳。
    4.5 弱引用

    通过前几个示例我们会发现如果我们一旦疏忽,就会容易地引发内存走漏的标题,为此,在ES6中为我们新增了两个有效的数据结构WeakMap和WeakSet,就是为了管理内存走漏的标题而诞生的。其表现弱引用,它的键名所引用的对象均是弱引用,弱引用是指垃圾接纳的过程中不会将键名对该对象的引用思量进去,只要所引用的对象没有其他的引用了,垃圾接纳机制就会开释该对象所占用的内存。这也就意味着我们不须要关心WeakMap中键名对其他对象的引用,也不须要手动地举行引用扫除,我们实验在node中演示一下过程(参考阮一峰ES6尺度入门中的示例,自己手动实现了一遍)。
    起首打开node下令行,输入以下下令:
    1. node --expose-gc // --expose-gc 表现答应手动实验垃圾接纳机制
    复制代码
    然后我们实验下面的代码。
    1. // 手动实验一次垃圾接纳包管内存数据准确> global.gc();undefined// 检察当前占用的内存,重要关心heapUsed字段,巨细约为4.4MB> process.memoryUsage();{ rss: 21626880,  heapTotal: 7585792,  heapUsed: 4708440,  external: 8710 }// 创建一个WeakMap> let wm = new WeakMap();undefined// 创建一个数组并赋值给变量key> let key = new Array(1000000);undefined// 将WeakMap的键名指向该数组// 此时该数组存在两个引用,一个是key,一个是WeakMap的键名// 注意WeakMap是弱引用> wm.set(key, 1);WeakMap { [items unknown] }// 手动实验一次垃圾接纳> global.gc();undefined// 再次检察内存占用巨细,heapUsed已经增长到约12MB> process.memoryUsage();{ rss: 30232576,  heapTotal: 17694720,  heapUsed: 13068464,  external: 8688 }// 手动扫除变量key对数组的引用// 注意这里并没有扫除WeakMap中键名对数组的引用> key = null;null// 再次实验垃圾接纳> global.gc()undefined// 检察内存占用巨细,发现heapUsed已经回到了之前的巨细(这里约为4.8M,原来为4.4M,稍微有些浮动)> process.memoryUsage();{ rss: 22110208,  heapTotal: 9158656,  heapUsed: 5089752,  external: 8698 }
    复制代码
    在上述示例中,我们发现固然我们没有手动扫除WeakMap中的键名对数组的引用,但是内存仍旧已经回到原始的巨细,阐明该数组已经被接纳,那么这个也就是弱引用的详细寄义了。
    5、总结

    本文中重要解说了一下V8引擎的垃圾接纳机制,并分别重新生代和老生代陈诉了差别分代中的垃圾接纳计谋以及对应的接纳算法,之后列出了几种常见的克制内存走漏的方式来资助我们写出更加优雅的代码。如果你已经相识过垃圾接纳干系的内容,那么这篇文章可以资助你简朴复习加深印象,如果没有相识过,那么笔者也盼望这篇文章可以或许资助到你相识一些代码层面之外的底层知识点,由于V8引擎的源码是用C++实现的,以是笔者也就没有做这方面的深入了,有爱好的小同伴儿可以自行探究,文中有错误的地方,还盼望可以或许在品评区指正。
    6、交换

    如果你以为这篇文章的内容对你有资助,能否帮个忙关注一下笔者的公众号[前端之境],每周都会积极原创一些前端技醒目货,关注公众号后可以邀你参加前端技能交换群,我们可以一起相互交换,共同进步。
    文章已同步更新至Github博客,若觉文章尚可,欢迎前往star!
    你的一个点赞,值得让我付出更多的积极!
    窘境中发展,只有不停地学习,才能成为更好的自己,与君共勉!
    090319l1pgx02t70zq0p1p.png
    C#论坛 www.ibcibc.com IBC编程社区
    C#
    C#论坛
    IBC编程社区
    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则