介绍
每种编程语言都有其内存管理机制,例如简单的C有低级内存管理原语,如malloc()、free()。 同样,我们在学习JavaScript的时候,有必要了解JavaScript的内存管理机制。 JavaScript的内存管理机制是:内存原语在变量(对象、字符串等)创建时分配,然后在不再使用时“自动”释放。 后者称为垃圾收集。 这种“自动”令人困惑,并给 JavaScript(和其他高级语言)开发人员一种他们可以忘记内存管理的错觉。 对于后端开发来说,内存空间并不是一个经常被提及的概念,也很容易被你忽略。 当然也包括我自己。 一直以来,我都觉得显存空间的概念在JS的学习中并不是那么重要。 但是后来,当我回去重新整理JS基础的时候,我发现因为我对它们的理解比较模糊,所以很多东西我都看不懂。 比如最基本的引用数据类型和引用传递是什么? 例如,浅拷贝和深拷贝有什么区别? 还有闭包、原型等等。 但看来在使用JavaScript进行开发的过程中,了解JavaScript的内存机制可以帮助开发者清楚地了解自己编写的代码在执行过程中发生了什么,从而提高项目的代码质量。
记忆模型
JS内存空间分为栈(stack)、堆(heap)、池(一般也归类为栈)。 栈存储变量,堆存储复杂对象javascript 全局变量定义,池存储常量。
基本数据类型和堆栈内存
JS中的基本数据类型,这些值都有固定的大小,往往存储在栈内存中(闭包除外),存储空间由系统手动分配。 我们可以直接操作栈内存空间中存储的值,因此基本数据类型都是按值访问。 栈内存中数据的存储和使用与数据结构中的栈数据结构类似,遵循后进先出的原则。 基本数据类型:数字字符串 Null 未定义 Boolean
为了简单理解栈内存空间的存储形式,我们可以用乒乓球包类比来分析。 乒乓球5在包里最上面一层,它必须放在最后,但可以先用。 而如果我们想使用底部的乒乓球1,就必须把里面的4个乒乓球取下来,这样乒乓球1就在包的最上面一层了。 这就是栈空间的先进后出和后进先出的特性。
参考数据类型和堆内存
与其他语言不同的是,JS的引用数据类型,比如链表Array,它们的值的大小并不是固定的。 引用数据类型的值是存储在堆内存中的对象。 JS不允许直接访问堆内存中的位置,所以我们无法直接操作对象的堆内存空间。 当操作一个对象时,您实际上是在操作对该对象的引用,而不是实际的对象。 因此,引用类型的值是通过引用来访问的。 这里的引用可以理解为栈内存中存储的地址,与堆内存的实际值相关联。
堆叠和访问数据的方法与书柜和书籍的方法非常相似。 书其实是有序地存放在书柜上的,但是只要我们知道书的名字,我们就可以轻松地取出我们想要的书,而不是像拿乒乓球一样把里面的书都拿出来。乒乓球包。 取出乒乓球,就可以得到中间的某个乒乓球。 例如,在JSON格式的数据中,我们存储的键值可以是无序的,因为顺序的不同并不影响我们的使用,我们只需要关心书名即可。
为了更好的理解栈内存和堆内存,我们可以结合下面的例子和图来进行理解。
var a1 = 0; // 栈 var a2 = 'this is string'; // 栈 var a3 = null; // 栈 var b = { m: 20 }; // 变量b存在于栈中,{m: 20} 作为对象存在于堆内存中 var c = [1, 2, 3]; // 变量c存在于栈中,[1, 2, 3] 作为对象存在于堆内存中
变量名具体值c0x0012ff7db0x0012ff7ca3nulla2this is stringa10
堆空间
[1,2,3]
{米:20}
所以当我们要访问堆内存中的引用数据类型时,实际上是先从栈中获取对象的地址引用(或地址指针),然后再从堆内存中获取我们需要的数据。 了解了JS的内存空间之后,我们就可以利用内存空间的特性来验证引用类型的一些特性。我们在后端笔试中经常会遇到这样类似的问题
// demo01.js var a = 20; var b = a; b = 30; // 这时a的值是多少? // demo02.js var m = { a: 10, b: 20 }; var n = m; n.a = 15; // 这时m.a的值是多少
当栈内存中的数据被复制时,系统会手动为新变量赋一个新值。 var b = a执行后,即使a和b的值都等于20,但它们已经是独立的,彼此独立了。 详细信息如图所示。 所以我们改变b的值后,a的值不会改变。
复制之前
堆栈内存空间值a 20
复制后
堆栈内存空间值 b 20 a 20
修改b的值后
堆栈内存空间值 b 30 a 20
在demo02中,我们通过var n = m来执行复制引用类型的操作。 引用类型的副本也会手动给新变量赋值一个新值,并保存在栈内存中,但不同的是,这个新值只是引用类型的地址指针。 当地址指针相同时,虽然彼此独立,但在堆内存中访问的具体对象实际上是相同的。
复制之前
栈内存空间 变量名 堆内存空间 m0x0012ff7d {a:10,b:20}
复制后
栈内存空间 变量名 堆内存空间 m0x0012ff7d {a:10,b:20}n0x0012ff7e
修改后
栈内存空间 变量名 堆内存空间 m0x0012ff7d{a:15,b:20}n0x0012ff7e
内存生命周期
JS环境中分配的显存通常有以下生命周期:
为了便于理解,我们用一个简单的例子来解释这个循环。
var a = 20; // 在内存中给数值变量分配空间 alert(a + 100); // 使用内存 var a = null; // 使用完毕之后,释放内存空间
我们非常了解第一步和第二步。 JavaScript在定义变量时完成显存分配。 第三步释放显存空间是我们需要重点理解的一点。
现在想一下,从显存的角度来看,null和undefined有什么本质区别呢?
为什么 typeof(null) //object typeof(undefined) //undefined?
ES6 语法中的 const 声明一个只读常量。 一旦声明,常量的值就不能更改。 但是下面的代码可以改变const的值,为什么呢?
const foo = {}; foo.prop = 123; foo.prop // 123 foo = {}; // TypeError: "foo" is read-only
记忆恢复
JavaScript有手动垃圾回收机制,那么这种手动垃圾回收机制的原理是什么呢? 其实很简单,就是找出这些不再使用的值,然后释放它们占用的显存。 垃圾收集器会每隔固定的时间执行一次空闲操作。
在JavaScript中,最常用的方法就是利用标记清理的算法来找出哪些对象不再被使用,所以a = null实际上只是一个释放引用的操作,这样a原来对应的值就失去了引用,已退出执行。 环境,下次垃圾收集器执行操作时将找到并释放该值。 在正确的时间取消引用是获得更好页面性能的重要形式。
function fun1() { var obj = {name: 'csa', age: 24}; } function fun2() { var obj = {name: 'coder', age: 2} return obj; } var f1 = fun1(); var f2 = fun2();
在上面的代码中,当 var f1 = fun1(); 执行时,执行环境会创建一个对象{name:'csa',age:24},此时var f2 = fun2(); 执行时,执行环境会创建一个对象{name:'coder',age=2},然后当下一次垃圾回收临近时,会释放该对象{name:'csa',age:的显存: 24},但是不会释放对象{name:'coder',age:2}的显存。 这是因为fun2()函数上返回的是对象{name:'coder,age:2'},其引用参数给了f2变量,而由于f2对象属于全局变量,所以不是在页面卸载的情况下,f2指向的对象{name:'coder',age:2}不会被回收。由于JavaScript语言的特殊性(闭包……),变得异常困难判断一个对象是否会被回收
垃圾收集算法
对于垃圾回收算法来说,核心思想是如何判断显存不再被使用。
引用计数算法
熟悉或者做过C语言的朋友都明白,引用无非是一个指向对象的指针。 对于那些不熟悉这种语言的人来说,引用可以简单地视为一个对象访问另一个对象的路径。 (这里的对象是一个笼统的概念,指的是JS环境中的实体)。
引用计数算法定义“内存不再使用”的标准很简单,就是看一个对象是否有引用指向它。 如果没有其他对象指向它,则不再需要该对象。
// 创建一个对象person,他有两个指向属性age和name的引用 var person = { age: 12, name: 'aaaa' }; person.name = null; // 虽然设置为null,但因为person对象还有指向name的引用,因此name不会回收 var p = person; person = 1; //原来的person对象被赋值为1,但因为有新引用p指向原person对象,因此它不会被回收 p = null; //原person对象已经没有引用,很快会被回收
从前面可以看出,引用计数算法是一种简单而有效的算法。 但它有一个致命的问题:循环引用。 如果两个对象互相引用,即使不再使用,垃圾收集器也不会回收,从而导致内存窃取。
function cycle() { var o1 = {}; var o2 = {}; o1.a = o2; o2.a = o1; return "Cycle reference!" } cycle();
上面我们声明了一个循环多项式,其中包含两个互相引用的对象。 调用函数结束后,对象o1和o2实际上已经离开了函数的作用域,因此不再需要。 但是根据引用计数的原理,它们之间的相互引用仍然存在,所以这部分显存不会被回收,内存泄漏是不可避免的。
正是由于这个严重的缺点,该算法早已被现代浏览器中引入的标记消除算法所取代。 但千万不能认为这个问题早已不存在了,因为仍然占据很大市场的IE的祖先就使用了这种算法。当需要照顾兼容性时,一些看似常见的写法也可能会导致意想不到的问题
var div = document.createElement("div"); div.onclick = function() { console.log("click"); };
上面的JS写法都是很常见的。 创建一个 DOM 元素并绑定单击事件。 那么这里存在哪些问题呢? 请注意,变量 div 具有对事件处理程序的引用,并且风暴处理程序也具有对 div 的引用! (div 变量可以在函数内部访问)。 出现了顺序引用,根据上面提到的算法,这部分显存不可避免地会泄漏。
现在你明白为什么后端程序员讨厌 IE 了吧? bug很多,仍然占据很大市场的IE,是后端开发的终生敌人! 亲爱的,没有买卖就没有杀戮。
标记去除算法
如上所述,现代浏览器不再使用引用计数算法。 现代浏览器大多是基于标记消除算法的个别改进算法,总体思路是相同的。
标记删除算法将“不再使用的对象”定义为“无法访问的对象”。 简单来说,就是从内部(JS中是全局对象)开始,定期扫描显存中的对象。 任何可以从内部触及的东西仍然需要使用。 难以从内部访问的对象被标记为不再使用并稍后回收。
从这个概念可以看出,不可触摸对象包含了没有引用的对象的概念(没有任何引用的对象也是不可触摸对象)。 但反之亦然可能不成立。
根据这个概念,上面的例子就可以通过垃圾回收来正确处理。 当div及其时间处理函数无法再从全局对象中触及时,垃圾收集器将标记并回收这两个对象。
如何写出对内存管理友好的JS代码?
如果你仍然需要兼容旧的浏览器,那么你需要注意代码中的循环引用问题。 或者直接采用保证兼容性的库来帮助优化代码。
对于现代浏览器来说,唯一需要注意的就是明确切断需要回收的对象与内部的联系。 有时这些链接并不重要,并且由于标记删除算法的稳健性,出现此问题的可能性较小。最常见的内存泄漏通常与 DOM 元素绑定有关
email.message = document.createElement(“div”); displayList.appendChild(email.message); // 稍后从displayList中清除DOM元素 displayList.removeAllChildren();
div 元素已从 DOM 树中删除,这意味着无法从 DOM 树内部触及 div 元素。 但请注意javascript 全局变量定义,div 元素也绑定到电子邮件对象。 因此,只要电子邮件对象存在,div 元素就会保留在视频内存中。
如果你的引用只包含少量的 JS 交互,那么内存管理不会让你太困惑。 一旦开始构建中型到大型 SPA 或服务器和桌面应用程序,内存泄漏就应该提上日程。 不要满足于编写一个可以运行的程序,也不要认为升级机器就可以解决一切。
内存泄漏
什么是内存泄漏
对于持续运行的服务进程(守护进程)来说,未使用的显存必须及时释放。 否则内存占用会越来越高,轻则影响系统性能,重则导致进程崩溃。 如果不再使用的显存没有及时释放,则称为内存泄漏。 有些语言(如C语言)必须自动释放显存,由程序员负责显存管理。
char * buffer; buffer = (char*) malloc(42); // Do something with buffer free(buffer);
以上是C语言代码。 malloc方法用于申请显存。 使用完毕后,必须使用free方法释放显存。 这就很麻烦了,所以大多数语言都提供手动内存管理来减轻程序员的负担,这就是所谓的“垃圾收集器”(garbagecollector)
如何观察内存泄漏? 经验法则是,如果连续五次垃圾回收后,内存使用量每次都变大,则存在视频内存泄漏。 (咳咳,别装酷了)这就需要我们实时查看显存使用情况
浏览器方法
如果显存使用量基本稳定并接近水平,则说明不存在内存泄漏。 否则就是内存泄漏。
命令行
命令行可以使用Node提供的process.memoryUsage方法。
console.log(process.memoryUsage()); // { // rss: 27709440, // heapTotal: 5685248, // heapUsed: 3449392, // external: 8772 // }
process.memoryUsage 返回一个对象,其中包含 Node 进程的内存使用信息。 该对象包含四个数组,单位为字节,含义如下。
判断内存泄漏以heapUsed数组为准。
弱映射
正如前面提到的,及时清理引用非常重要。 然而你不可能记住这么多,而且有时记不住就忘记了,所以才会出现这么多的显存泄漏。
最好有一种方法来声明哪些引用在创建新引用时必须自动消除,哪些引用可以忽略。 当其他引用消失时,垃圾收集机制可以释放视频内存。 只有这样才能大大减轻程序员的负担,只需要消除主要引用即可。
ES6考虑到了这一点,引入了两种新的数据结构:WeakSet和WeakMap。 它们对值的引用不包含在垃圾收集机制中,因此名称上会有一个“Weak”,表明这是一个弱引用。
const wm = new WeakMap(); const element = document.getElementById('example'); wm.set(element, 'some information'); wm.get(element) // "some information"
上面的代码中,首先创建一个Weakmap实例。 然后,将一个 DOM 节点作为键值存储到实例中,并将一些附加信息作为通配符一起存储在 WeakMap 中。 此时对WeakMap中元素的引用就是弱引用,不会被记录在垃圾回收机制中。
即DOM节点对象的引用计数为1,而不是2。此时,一旦移除对该节点的引用,其占用的显存就会被垃圾回收机制释放。 Weakmap 保存的通配符对也会手动消失。
基本上,如果你想向对象添加数据并且不想干扰垃圾收集机制,你可以使用WeakMap。