JavaScript中的垃圾回收🚀
数据在内存中的存放
要了解垃圾回收处理,我们先浅浅谈一下JavaScript 中的数据以及如何存储,毕竟垃圾回收的前提得先制造垃圾。
语言类型
静态语言
:在使用之前就需要确认其变量数据类型
动态语言
:在运行过程中需要检查数据类型的语言
比如我们所讲的 JavaScript 就是动态语言,因为在声明变量之前并不需要确认其数据类型。
有的时候我们把number类型的变量赋值给了 bool 型的变量 ,一些编译器会把number 型的变量悄悄转换为 bool 型的变量,我们通常把这种偷偷转换的操作称为隐式类型转换。
强类型语言
:支持隐式类型转换的语言
弱类型语言
:不支持隐式类型转换的语言
在这点上, JavaScript 是弱类型语言。
JavaScript 是一种弱类型的、动态的语言
- 弱类型意味着你不需要告诉 JavaScript 引擎这个或那个变量是什么数据类型,JavaScript 引擎在运行代码的时候自己会计算出来。
- 动态意味着你可以使用同一个变量保存不同类型的数据。
javaScript的数据类型
javaScript的数据类型一共有8
种,分为两类。
一类是基本数据类型
,也叫简单数据类型,包含7种类型,分别是Number
、String
、Boolean
、BigInt
、Symbol
、Null
、Undefined
。
另一类是引用数据类型
也叫复杂数据类型,通常用Object
代表,普通对象,数组,正则,日期,Math数学函数都属于Object。
数据分成两大类的本质区别:
基本数据类型和引用数据类型它们在内存中的存储方式不同
。
基本数据类型是直接存储在栈中的简单数据段,占据空间小,属于被频繁使用的数据。
引用数据类型是存储在堆内存中,占据空间大。引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址,当解释器寻找引用值时,会检索其在栈中的地址,取得地址后从堆中获得实体。
Symbol
是ES6新出的一种数据类型,这种数据类型的特点就是没有重复的数据,可以作为object
的key。 数据的创建方法Symbol(),因为它的构造函数不够完整,所以不能使用new Symbol()
创建数据。由于Symbol()创建数据具有唯一性,所以 Symbol() !== Symbol()
, 同时使用Symbol数据作为key不能使用for获取到这个key,需要使用Object.getOwnPropertySymbols(obj)
获得这个obj对象中key类型是Symbol的key值。
let key = Symbol('key');
let obj = { [key]: 'symbol'};
let keyArray = Object.getOwnPropertySymbols(obj); // 返回一个数组[Symbol('key')]
obj[keyArray[0]] // 'symbol'
BigInt
也是ES6新出的一种数据类型,这种数据类型的特点就是数据涵盖的范围大,能够解决超出普通数据类型范围报错的问题。
使用方法:
- 整数末尾直接+n:647326483767797n
- 调用BigInt()构造函数:BigInt("647326483767797")
注意:BigInt和Number之间不能进行混合操作
有一些小问题大家自行了解一下
- 在V8中,除了小的整数之外(Smi), 其他类型,包括string,都是在Heap上。另外像数字类型,很多时候也是在Heap上。具体参考: v8.dev/blog/react-…
"For small integers in the 31-bit signed integer range, V8 uses a special representation called Smi. Anything that is not a Smi is represented as a HeapObject, which is the address of some entity in memory. " 这里v8的文档里面也有讲: developer.chrome.com/docs/devtoo…
- "原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址。" 这也是存在一定的问题。因为string interning的存在,string literals都存在constant pool里,
const a = 'foo'; const b =
foo这里内存里面不会有两个字符串
foo`, 被复制不是string的值,而是constant pool pointer. 这些在v8的design doc里面有讲。docs.google.com/document/d/…
包装对象
基本数据类型临时创建的临时对象,称为包装对象。其中number,boolean,string有包装对象,代码运行的过程中会找到对应的包装对象,然后包装对象把属性和方法给了基本类型,然后包装对象被系统进行摧毁.
提一嘴:typeof null==object
在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null
代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签是 0,typeof null
也因此返回 "object"
typeof可能的一些返回值
原理:
不同的对象在底层都表示为二进制,在Javascript中二进制前(低)三位存储其类型信息。
000: 对象
010: 浮点数
100:字符串
110:布尔
1: 整数
typeof null 为"object"
, 原因是不同的对象在底层都表示为二进制,在Javascript中二进制前(低)三位都为0的话会被判断为Object类型,null的二进制表示全为0,自然前三位也是0,所以执行typeof
时会返回"object"。
垃圾回收策略
上面我们提到了JavaScript 中的数据以及如何存储,不过有些数据被使用之后,可能就不再需要了,我们把这种数据称为垃圾数据。如果这些垃圾数据一直保存在内存中,那么内存会越用越多,所以我们需要对这些垃圾数据进行回收,以释放有限的内存空间。
不同语言的垃圾回收策略(自动与非自动)
通常情况下,垃圾数据回收分为手动回收和自动回收两种策略。
手动垃圾回收
:何时分配内存、何时销毁内存都是由代码控制的,如 C/C++ 等语言
自动垃圾回收
:产生的垃圾数据是由垃圾回收器来释放的,并不需要手动通过代码来释放。如 JavaScript、Java、Python 等语言
垃圾回收机制概述
浏览器的 Javascript 具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存。
原理:垃圾收集器会定期(周期性)找出那些不再继续使用的变量,然后释放其内存。但是这个过程不是实时的,因为其开销比较大并且GC时停止响应其他操作,所以垃圾回收器会按照固定的时间间隔周期性的执行。
作用:js的垃圾回收机制是为了防止内存泄漏。
内存泄漏:指程序中动态分配的一块内存因为某种原因无法释放,造成系统内存浪费,造成系统内存浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
垃圾回收机制
因为数据是存储在栈和堆两种内存空间中的,所以接下来我们就来分别介绍“栈中的垃圾数据”和“堆中的垃圾数据”是如何回收的。
调用栈的数据回收
原始类型的数据被分配到栈中,引用类型的数据会被分配到堆中。
当某一个函数执行结束之后,该函数的执行上下文会从堆中被销毁掉,那么它是怎么被销毁的呢?
其实还有一个记录当前执行状态的指针(称为 ESP),当上一个函数执行完成之后,函数执行流程就进入了当前 函数,那这时就需要销毁上一个函数的执行上下文了。所以JavaScript 会将 ESP 下移到当前函数的执行上下文
,这个下移操作就是销毁 上一个函数执行上下文的过程。
为啥 ESP 下移就可以达到这种效果呢?
当上一个函数执行结束之后,ESP 向下移动到 当前函数的执行上下文中,上一个函数的执行上下文虽然保存在栈内存中,但是已经是无效内存了。比如当 foo 函数再次调用另外一个函数时,这块内容会被直接覆盖掉,用来存放另外一个函数的执行上下文
。
简单地说就是:当一个函数执行结束之后,JavaScript 引擎会通过向下移动 ESP 来销毁该函数保存在栈中的执行上下文。
堆中的数据回收
要回收堆中的垃圾数据,就需要用到 JavaScript 中的垃圾回收器了。我们先了解一些垃圾收集算法,通常,垃圾回收算法有很多种,但是并没有哪一种能胜任所有的场景,你需要权衡各种场景,根据对象的生存周期的不同而使用不同的算法,以便达到最好的效果。
标记清除法
这是目前主流的垃圾收集算法,将当前不使用的值加上标记,然后进行内存回收。(通过变量在执行环境中的生存周期)
分为标记和清除两个阶段:首先标记出所有的需要回收的对象,在标记完成以后统一回收所有被标记的对象。
整个标记清除算法大致过程就像下面这样:
- 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0
- 然后从各个根对象开始遍历,而不是垃圾的节点改成1
- 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间
- 最后,把素有内存=中对象标记为0,等待下一轮垃圾回收
在标记清除时,如何确定变量不再被使用?
JS中分为全局执行环境和函数的执行环境,代码运行时,会先将全局执行环境压入栈中,然后当执行流进入函数时,会创建函数的执行环境和相应的作用域链。 每个函数都有自己的执行环境,对应着相应的作用域链。对变量的查找和赋值都沿着自己的作用域链向上查找。 创建函数时,会创建一个预先包含全局变量对象的作用域链;当执行函数时,会创建本地活动对象(包括arguments和局部变量),并将其推入作用域链前端。当执行函数时,对变量的查找会沿着作用域链向上搜索,直到作用域链的最顶端—全局变量对象。当函数执行完毕后,本地活动对象会在内存中销毁。而闭包中,当外层函数执行完毕后,外层函数的作用域链被销毁,但被闭包函数引用的活动对象还存在内存中。
不确定:任何函数(包括闭包函数)在执行完毕后,只能销毁本地活动对象,所以为了销毁闭包引用的活动对象,需要销毁闭包函数,即解除引用null。
缺点:
(1)内存碎片化,空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块。
(2)分配速度慢,因为即便是使用 First-fit
策略,其操作仍是一个 O(n)
的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢。
那如何找到合适的块呢?我们可以采取下面三种分配策略
- First-fit,找到大于等于size的块立即返回
- Best-fit,遍历整个空闲列表,返回大于等于size的最小分块
- Worst-fit,遍历整个空闲列表,找到最大的分块,然后切成两部分,一部分size大小,并将该部分返回
标记整理法
归根结底,标记清除算法的缺点在于清除之后剩余的对象位置不变而导致的空闲内存不连续
,所以只要解决这一点,两个缺点都可以完美解决了
而标记整理(Mark-Compact)算法
就可以有效地解决,它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存(如下图)
引用计数法
把 对象是否不再需要
简化定义为 对象有没有其他对象引用到它
,如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
它的策略是跟踪记录每个变量值被使用的次数。
- 当声明了一个变量并将一个引用类型复制给该变量的时候这个值得分引用次数就为1
- 如果同一个值又被赋给另一个变量,那么引用数加1
- 如果该变量的值被其他的值覆盖了,则引用次数减1
- 如果这个值的引用次数变为0的时候,说明没有变量在使用,这个值没法访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为0的值占用的内存。
缺点:
(1)需要一个计数器,而此计数器需要占很大的位置,因为我们也不知道被引用数量的上限
(2)无法解决循环引用无法回收的问题
V8对垃圾回收的优化
代际假说和分代收集
代际假说(The Generational Hypothesis)的内容,这是垃圾回收领域中一个重要的术语,后续垃圾回收的策略都是建立在该假说的基础之上的,所以很是重要。
代际假说有以下两个特点:
第一个是大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经分配内存,很快就变得不可访问;
第二个是不死的对象,会活得更久。
在 V8 中会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。
新生区通常只支持 1~8M 的容量,而老生区支持的容量就大很多了。
对于这两块区域,V8 分别使用两个不同的垃圾回收器,以便更高效地实施垃圾回收。
- 副垃圾回收器,主要负责新生代的垃圾回收。
- 主垃圾回收器,主要负责老生代的垃圾回收。
垃圾回收器的工作流程
不论什么类型的垃圾回收器,它们都有一套共同的执行流程。
- 第一步是标记空间中活动对象和非活动对象。所谓活动对象就是还在使用的对象,非活动对象就是可以进行垃圾回收的对象。
- 第二步是回收非活动对象所占据的内存。其实就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。
- 第三步是做内存整理。一般来说,频繁回收对象后,内存中就会存在大量不连续空间,我们把这些不连续的内存空间称为内存碎片。当内存中出现了大量的内存碎片之后,如果需要分配较大连续内存的时候,就有可能出现内存不足的情况。所以最后一步需要整理这些内存碎片,但这步其实是可选的,因为有的垃圾回收器不会产生内存碎片
新生代垃圾回收
新生代空间中的对象为存活时间较短的对象,大多数的对象被分配在这里,这个区域很小但是垃圾会特别频繁 。
Cheney
算法将堆内存一分为二,一个是处于使用状态的空间我们暂且称为使用区
,一个是处于闲置状态的空间我们称为空闲区
,如下图所示。
对于新产生的对象,将从使用区空间中分配内存 。
新生代分配内存非常容易,我们只需要保存一个指向内存区的指针,不断根据新对象的大小进行递增即可。当该指针到达了新生代内存区的末尾,就会触发一次垃圾回收。
新生代的垃圾回收采用 Scavenge 算法 ,其工作原理如下:
当开始进行垃圾回收时,新生代垃圾回收器会对使用区中的活动对象做标记,标记完成之后将使用区的活动对象复制进空闲区并进行排序,随后进入垃圾清理阶段,即将非活动对象占用的空间清理掉。最后进行角色互换,把原来的使用区变成空闲又,把原来的空闲又变成使用。
当一个对象经过多次复制后依然存活,它将会被认为是生命周期较长的对象,随后会被移动到老生代中,采用老生代的垃圾回收策略进行管理。
另外还有一种情况,如果复制一个对象到空闲区时,空闲区空间占用超过了.25%,那么这个对象会被直接普升到老生代空间中,设置为 25%的比例的原因是,当完成Scavenge 回收后,空闲区将翻转成使用区,继续进行对象内存的分配,若占比过大,将会影响后续内存分配。
反思:
-
转的操作还能让新生代中的这两块区域无限重复使用下去。
-
生代中采用的 Scavenge 算法,所以每次执行清理操作时,都需要将存活的对象从对象区域复制到空闲区域。但复制操作需要时间成本,如果新生区空间设置得太大了,那么每次清理的时间就会过久,所以为了执行效率,一般新生区的空间会被设置得比较小。也正是因为新生区的空间不大,所以很容易被存活的对象装满整个区域。为了解决这个问题,JavaScript 引擎采用了对象晋升策略,也就是经过两次垃圾回收依然还存活的对象,会被移动到老生区中。
老生代垃圾回收
主垃圾回收器主要负责老生区中的垃圾回收。除了新生区中晋升的对象,一些大的对象会直接被分配到老生区。
因此老生区中的对象有两个特点,一个是对象占用空间大,另一个是对象存活时间长。
由于老生区的对象比较大,若要在老生区中使用 Scavenge 算法进行垃圾回收,复制这些大的对象将会花费比较多的时间,从而导致回收执行效率不高,同时还会浪费一半的空间。
因此,V8 对于老生代中的垃圾回收,采用 Mark-Sweep (标记清除) 和 Mark-Compact(标记整理) 相结合 。
(1) Mark-Sweep
Mark-Sweep 分为 标记 和 清除 两个阶段 。
首先是在标记阶段,需要遍历堆中的所有对象,并标记那些活着的对象,也即是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。
然后进入清除阶段。在清除阶段,只清除没有被标记的对象。由于标记清除只清除死亡对象,而死亡对象在老生代中占用的比例很小,所以效率较高。
标记清除存在的问题是,进行一次标记清除后,内存空间往往是不连续的,会出现很多的内存碎片。如果后续需要分配一个需要内存空间较多的对象时,如果所有的内存碎片都不够用,将会使得V8无法完成这次分配,提前触发垃圾回收。
所以产生了Mark-Compact
(2) Mark-Compact
标记整理正是为了解决标记清除所带来的内存碎片的问题。标记整理在标记清除的基础进行修改,将其的清除阶段变为紧缩极端。在整理的过程中,将活着的对象向内存区的一段移动,移动完成后直接清理掉边界外的内存。
也即是这个标记过程仍然与标记 - 清除算法里的是一样的,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
紧缩过程涉及对象的移动,所以效率并不是太好,但是能保证不会生成内存碎片。
对于三种回收策略比较
从图中可以看出,在 Mark-Sweep 和 Mark-Compact 之间,由于 Mark-Compact 需要移动对象,所以它的执行速度最慢。
所以在取舍上,V8 主要使用 Mark-Sweep,在空间不足以对新生代中晋升过来的对象进行分配时才使用 Mark-Compact 。
垃圾回收引起的性能问题
由于 JavaScript 是运行在主线程之上的,为了避免出现 JavaScript 应用逻辑 与 垃圾回收操作 产生不一致的冲突,垃圾回收的三种基本算法都需要将应用逻辑暂停下来,待垃圾回收完成后,再恢复执行应用逻辑,这种行为被称为全停顿 。
官方说法,以 1.5G 的垃圾回收堆内存为例,V8 做一次小的垃圾回收需要 50ms 以上,做一次非增量式垃圾回收甚至需要 1s 以上。这是垃圾回收中引起的 JavaScript 线程暂停执行时间,在这样的时间花销下,应用性能和响应能力都会直线下降。
比如一次GC需要60ms,那我们的应用逻辑就得暂停60ms,假如一次GC的时间过长,对用户来说就可能会造成页面卡顿问题。
在 V8 的分代式垃圾回收中,一次小垃圾回收只收集新生代,由于新生代默认配置的较小,且其中活动对象通常较少,所以即便它是全停顿,影响也不大。但是也有相应的应对措施。
并行回收(Parallel)
所谓并行,指的是垃圾回收器在主线程上执行的过程中,开启多个辅助线程,同时执行同样的回收工作。
简单来说,使用并行回收,假如本来是主线程一个人干活,它一个人需要 3秒,现在叫上了 2个辅助线程和主线程一块干活,那三个人一块干一个人干1秒就完事了,但是由于多人协同办公,所以需要加上一部分多人协同(同步开销)的时间我们算 0.5 秒好了,也就是说,采用并行策略后,本来要 3秒的活现在 1.5 秒就可以干完了。
不过虽然 1.5秒就可以干完了,时间也大大缩短了,但是这 1.5秒内,主线程还是需要让出来的,也正是因为主线程还是需要让出来,这个过程内存是静态的,不需要考虑内存中对象的引用关系改变,只需要考虑协同,实现起来也很简单。
新生代对象空间就采用并行策略
,在执行垃圾回收的过程中,会启动了多个线程来负责新生代中的垃圾清理操作,这些线程同时将对象空间中的数据移动到空闲区域,这个过程中由于数据地址会发生改变,所以还需要同步更新引用这些对象的指针,此即并行回收。
增量标记(Incremental Marking)算法
V8 的老生代通常配置较大,且存活对象较多,全堆垃圾回收的标记、清理、整理等动作造成的停顿就会比较严重。
为了降低老生代的垃圾回收而造成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,我们把这个算法称为增量标记算法。
增量就是将一次GC标记的过程,分成了很多小步,每执行完一小步就让应用逻辑停一会,这样交替多次后完成一轮GC标记(如下图)
试想一下,将一次完整的 Gc 标记分次执行,那在每一小次 Gc标记执行完之后如何暂停下来去执行任务程序,而后又怎么恢复呢?那假如我们在一次完整的,Gc标记分块暂停后,执行任务程序时内存中标记好的对象引用关系被修改了又怎么办呢?
可以看出增量的实现要比并行复杂一点,V8 对这两个问题对应的解决方案分别是三色标记法与写屏障。
三色标记法即使用每个对象的两个标记位和一个标记工作表来实现标记,两个标记位编码三种颜色:白、灰、黑
- 白色指的是未被标记的对象
- 灰色指自身被标记,成员变量(该对象的引用对象)未被标记。
- 黑色指自身和成品变最皆被标记
采用三色标记法后我们在恢复执行时就好办多了,可以直接通过当前内存中有没有灰色节点来判断整个标记
是否完成,如没有灰色节点,直接进入清理阶段,如还有灰色标记,恢复时直接从灰色的节点开始继续执行就可以。
三色标记法的 mark 操作可以渐进执行的而不需每次都扫描整个内存空间,可以很好的配合增量回收进行暂停恢复的一些操作,从而减少全停顿
的时间。
使用增量标记算法,可以把一个完整的垃圾回收任务拆分为很多小的任务,这些小的任务执行时间比较短,可以穿插在其他的 JavaScript 任务中间执行,这样当执行上述动画效果时,就不会让用户因为垃圾回收任务而感受到页面的卡顿了。
除了增量标记(Incremental Marking)算法和并行回收算法外,为降低全堆垃圾回收而导致的停顿时间,V8 做了以下改善措施:
限制堆内存大小
- 新生代:64 位系统 和 32 位系统分别为 32M 和 16 M (from 和 to 空间各占一半)
- 老生代:64 位系统 和 32 位系统分别为 1400M 和 700 M
V8 后续还引入 Lazy Sweep(延迟清除)、Incremental Compaction (增量式整理),让清理与整理动作也变成增量式的。同时还计划引入并行标记与并行整理,进一步利用多核性能来降低每次停顿的时间。
什么时候触发垃圾回收?
垃圾回收器周期性运行,如果分配的内存非常多,那么回收工作也会很艰巨,确定垃圾回收时间间隔就变成了一个值得思考的问题。
IE6的垃圾回收是根据内存分配量
运行的,当环境中存在256个变量、4096个对象、64k的字符串任意一种情况的时候就会触发垃圾回收器工作,看起来很科学,不用按一段时间就调用一次,有时候会没必要,这样按需调用不是很好吗?但是如果环境中就是有这么多变量等一直存在,现在脚本如此复杂,很正常,那么结果就是垃圾回收器一直在工作,这样浏览器就没法儿玩儿了。
微软在IE7中做了调整,触发条件不再是固定的,而是动态修改的
,初始值和IE6相同,如果垃圾回收器回收的内存分配量低于程序占用内存的15%,说明大部分内存不可被回收,设的垃圾回收触发条件过于敏感,这时候把临街条件翻倍,如果回收的内存高于85%,说明大部分内存早就该清理了,这时候把触发条件置回。这样就使垃圾回收工作智能了很多。
内存泄漏判断
- 一般是感官上的长时间运行页面卡顿,就可能会有内存泄漏。
- 通过
DynaTrace(IE)profiles
等工具一段时间收集数据,观察对象的使用情况。然后判断是否存在内存泄漏。修改后验证。 - 通过
chrome
的Perfomance
面板记录页面的活动,然后在页面上进行各种交互操作,过一段时间后(时间越长越好),停止记录,生成统计数据,然后看timeline
下部的内存变化趋势图,如果是有规律的周期平稳变化,则不存在内存泄漏,如果整体趋势上涨则说明存在内存泄漏。
内存泄漏原因以及解决办法
主要是队列消费不及时以及作用域未释放,具体可以从这几个方面入手:
- 大量的使用全局变量
- 忘记清除的定时器
- dom元素的引用
<button id="btn" type="button">按钮</button>
<script>
let btn = document.getElementById('btn')
btn.remove()
// 虽然按钮被删除页面上没有但是在打印btn console.log(btn) // <button id="btn" type="button">按钮</button> 已经被保存在内存里了 </script>
所以确定不使用的临时变量应该置为null。
在Vue中也有会有这些情况:
- 监听在
window/body
等事件没有解绑 - 绑在
EventBus
的事件没有解绑 - Vuex 的 $store watch 了之后没有 unwatch
- 使用第三方库创建,没有调用正确的销毁函数
解决办法:
- beforeDestroy 中及时销毁
- 绑定了 DOM/BOM 对象中的事件 addEventListener ,removeEventListener。
- 观察者模式 on,on,on,off 处理。
- 如果组件中使用了定时器,应销毁处理。
- 如果在 mounted/created 钩子中使用了第三方库初始化,对应的销毁。
- 使用弱引用 weakMap、weakSet。
顺便说一个在了解垃圾回收之前对闭包的误解,闭包会导致内存泄露吗?
正确的答案是不会。
内存泄露是指你用不到(访问不到)的变量,依然占居着内存空间,不能被再次利用起来。
闭包里面的变量就是我们需要的变量,不能说是内存泄露。
这个误解是如何来的?因为 IE。IE 有 bug,IE 在我们使用完闭包之后,依然回收不了闭包里面引用的变量。这是 IE 的问题,不是闭包的问题。
其他问题
WeakMap 和 WeakSet 是如何被回收的?
在 ES6 中为我们新增了两个数据结构 WeakMap、WeakSet,就是为了解决内存泄漏的问题,
它的键名所引用的对象都是弱引用,就是垃圾回收机制遍历的时候不考虑该引用。
只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。
也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。
不过,即使使用Map,过一段时间内存也会被释放掉,这是因为 javascript 引擎做了优化,会定期清理内存。
V8解析后的字节码或热节点的机器码是存在哪的?
V8解析后的字节码或热节点的机器码是存在哪的,是以缓存的形式存储的么?和浏览器三级缓存原理的存储位置比如内存和磁盘有关系么?
V8 引入了二进制代码缓存,通过把二进制代码保存在内存中来节约编译时间。
V8 使用两种代码缓存策略来缓存生成的代码:
首先,是 V8 第一次执行一段代码时,会编译源 JavaScript 代码,并将编译后的二进制代码缓存在内存中,我们把这种方式称为 内存缓存
(in-memory cache)。然后通过 JavaScript 源文件的字符串在内存中查找对应的编译后的二进制代码。这样当再次执行到这段代码时,V8 就可以直接去内存中查找是否编译过这段代码。如果内存缓存中存在这段代码所对应的二进制代码,那么就直接执行编译好的二进制代码。
其次,V8 除了采用将代码缓存在内存中策略之外,还会将代码缓存到硬盘
上,这样即便关闭了浏览器,下次重新打开浏览器再次执行相同代码时,也可以直接重复使用编译好的二进制代码。
实践表明,在浏览器中采用了二进制代码缓存的方式,初始加载时分析和编译的时间缩短了 20%~40%。
参考:
[1] 朴灵,深入浅出 Node.js,人民邮电出版社,2013
转载自:https://juejin.cn/post/7197010875400011836