🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[TOC] # 简介 从本质上说,内存泄漏可以定义为:不再被应用程序所需要的内存,出于某种原因,它不会返回到操作系统或空闲内存池中。 <br> # 四种常见的内存泄漏 ## 全局变量 JavaScript以一种有趣的方式处理未声明的变量: 对于未声明的变量,会在全局范围中创建一个新的变量来对其进行引用。在浏览器中,全局对象是window。例如: ~~~ function foo(arg) { bar = "some text"; } ~~~ 等价于: ~~~ function foo(arg) { window.bar = "some text"; } ~~~ 如果 bar 在 foo 函数的作用域内对一个变量进行引用,却忘记使用 var 来声明它,那么将创建一个意想不到的全局变量。在这个例子中,遗漏一个简单的字符串不会造成太大的危害,但这肯定会很糟。 <br> 创建一个意料之外的全局变量的另一种方法是使用this: ~~~ function foo() { this.var1 = "potential accidental global"; } // Foo自己调用,它指向全局对象(window),而不是未定义。 foo(); ~~~ <br> > 可以在JavaScript文件的开头通过添加“use strict”来避免这一切,它将开启一个更严格的JavaScript解析模式,以防止意外创建全局变量。 <br> 尽管我们讨论的是未知的全局变量,但仍然有很多代码充斥着显式的全局变量。根据定义,这些是不可收集的(除非被指定为空或重新分配)。用于临时存储和处理大量信息的全局变量特别令人担忧。如果你必须使用一个全局变量来存储大量数据,那么请确保将其指定为null,或者在完成后将其重新赋值。 <br> ## 被遗忘的定时器和回调 以`setInterval`为例,因为它在JavaScript中经常使用。 ~~~ var serverData = loadData(); setInterval(function() { var renderer = document.getElementById('renderer'); if(renderer) { renderer.innerHTML = JSON.stringify(serverData); } }, 5000); //每五秒会执行一次 ~~~ <br> 上面的代码片段演示了使用定时器时引用不再需要的节点或数据。 <br> renderer 表示的对象可能会在未来的某个时间点被删除,从而导致内部处理程序中的一整块代码都变得不再需要。但是,由于定时器仍然是活动的,所以,处理程序不能被收集,并且其依赖项也无法被收集。这意味着,存储着大量数据的 serverData 也不能被收集。 <br> 在使用观察者时,您需要确保在使用完它们之后进行显式调用来删除它们(要么不再需要观察者,要么对象将变得不可访问)。 <br> 作为开发者时,需要确保在完成它们之后进行显式删除它们(或者对象将无法访问)。 <br> 在过去,一些浏览器无法处理这些情况(很好的IE6)。幸运的是,现在大多数现代浏览器会为帮你完成这项工作:一旦观察到的对象变得不可访问,即使忘记删除侦听器,它们也会自动收集观察者处理程序。然而,我们还是应该在对象被处理之前显式地删除这些观察者。例如: ![](https://box.kancloud.cn/42b5d068292f712e35ead022716fd791_800x392.png) <br> 如今,现在的浏览器(包括IE和Edge)使用现代的垃圾回收算法,可以立即发现并处理这些循环引用。换句话说,在一个节点删除之前也不是必须要调用removeEventListener。 <br> 一些框架或库,比如JQuery,会在处置节点之前自动删除监听器(在使用它们特定的API的时候)。这是由库内部的机制实现的,能够确保不发生内存泄漏,即使在有问题的浏览器下运行也能这样,比如IE 6。 <br> ## 闭包 闭包是javascript开发的一个关键方面,一个内部函数使用了外部(封闭)函数的变量。由于JavaScript运行的细节,它可能以下面的方式造成内存泄漏: ![](https://box.kancloud.cn/df281f03c093b7f5a75330ae81e803fb_800x487.png) <br> 这段代码做了一件事:每次调用`replaceThing`的时候,`theThing`都会得到一个包含一个大数组和一个新闭包(someMethod)的新对象。同时,变量`unuse`d指向一个引用了``originalThing`的闭包。 <br> 是不是有点困惑了? 重要的是,一旦具有相同父作用域的多个闭包的作用域被创建,则这个作用域就可以被共享。 <br> 在这种情况下,为闭包`someMethod`而创建的作用域可以被`unused`共享的。`unused`内部存在一个对`originalThing`的引用。即使`unused`从未使用过,`someMethod`也可以在`replaceThing`的作用域之外(例如在全局范围内)通过`theThing`来被调用。 <br> 由于`someMethod`共享了`unused`闭包的作用域,那么`unused`引用包含的`originalThing`会迫使它保持活动状态(两个闭包之间的整个共享作用域)。这阻止了它被收集。 <br> 当这段代码重复运行时,可以观察到内存使用在稳定增长,当`GC`运行后,内存使用也不会变小。从本质上说,在运行过程中创建了一个闭包链表(它的根是以变量`theThing`的形式存在),并且每个闭包的作用域都间接引用了了一个大数组,这造成了相当大的内存泄漏。 <br> ## 脱离DOM的引用 有时,将DOM节点存储在数据结构中可能会很有用。假设你希望快速地更新表中的几行内容,那么你可以在一个字典或数组中保存每个DOM行的引用。这样,同一个DOM元素就存在两个引用:一个在DOM树中,另一个则在字典中。如果在将来的某个时候你决定删除这些行,那么你需要将这两个引用都设置为不可访问。 ![](https://box.kancloud.cn/6d82297a53a37105ab994e8b14ad0126_800x395.png) <br> 在引用 DOM 树中的内部节点或叶节点时,还需要考虑另外一个问题。如果在代码中保留对表单元格的引用(标记),并决定从 DOM 中删除表,同时保留对该特定单元格的引用,那么可能会出现内存泄漏。 <br> 你可能认为垃圾收集器将释放除该单元格之外的所有内容。然而,事实并非如此,由于单元格是表的一个子节点,而子节点保存对父节点的引用,所以对表单元格的这个引用将使**整个表保持在内存中**,所以在移除有被引用的节点时候要移除其子节点。 <br> <br> # 内存泄漏的识别方法 怎样可以观察到内存泄漏呢? [经验法则](https://www.toptal.com/nodejs/debugging-memory-leaks-node-js-applications)是,如果连续五次垃圾回收之后,内存占用一次比一次大,就有内存泄漏。这就要求实时查看内存占用。 <br> ## 浏览器 Chrome 浏览器查看内存占用,按照以下步骤操作 1. 打开开发者工具,选择 Performance 面板 2. 勾选 Memory 3. 点击左上角的录制按钮。 4. 在页面上进行各种操作,模拟用户的使用情况。 5. 一段时间后,点击对话框的 stop 按钮,面板上就会显示这段时间的内存占用情况。 <br> 如果内存占用基本平稳,接近水平,就说明不存在内存泄漏。 <br> ![](https://box.kancloud.cn/beace1717bf9da892dc04ecd9583e45d_600x307.png) <br> 反之,就是内存泄漏了。 <br> ![](https://box.kancloud.cn/44fd71c9d0297e10edd4099a8f13aa7f_599x307.png) <br> ## 命令行 命令行可以使用 Node 提供的[`process.memoryUsage`](https://nodejs.org/api/process.html#process_process_memoryusage)方法。 ~~~javascript console.log(process.memoryUsage()); // { rss: 27709440, // heapTotal: 5685248, // heapUsed: 3449392, // external: 8772 } ~~~ <br> `process.memoryUsage`返回一个对象,包含了 Node 进程的内存占用信息。该对象包含四个字段,单位是字节,[含义](http://stackoverflow.com/questions/12023359/what-do-the-return-values-of-node-js-process-memoryusage-stand-for)如下。 ![](https://box.kancloud.cn/03990390389f48dd7f3753a7be340841_550x543.png) <br> * rss(resident set size):所有内存占用,包括指令区和堆栈。 * heapTotal:"堆"占用的内存,包括用到的和没用到的。 * heapUsed:用到的堆的部分。 * external: V8 引擎内部的 C++ 对象占用的内存。 <br> 判断内存泄漏,以heapUsed字段为准。 <br> <br> # WeakMap 前面说过,及时清除引用非常重要。但是,你不可能记得那么多,有时候一疏忽就忘了,所以才有那么多内存泄漏。 <br> 最好能有一种方法,在新建引用的时候就声明,哪些引用必须手动清除,哪些引用可以忽略不计,当其他引用消失以后,垃圾回收机制就可以释放内存。这样就能大大减轻程序员的负担,你只要清除主要引用就可以了。 <br> ES6 考虑到了这一点,推出了两种新的数据结构:[WeakSet](http://es6.ruanyifeng.com/#docs/set-map#WeakSet)和[WeakMap](http://es6.ruanyifeng.com/#docs/set-map#WeakMap)。它们对于值的引用都是不计入垃圾回收机制的,所以名字里面才会有一个"Weak",表示这是弱引用。 <br> 下面以 WeakMap 为例,看看它是怎么解决内存泄漏的。 ~~~ const wm = new WeakMap(); const element = document.getElementById('example'); wm.set(element, 'some information'); wm.get(element) // "some information" ~~~ <br> 上面代码中,先新建一个 Weakmap 实例。然后,将一个 DOM 节点作为键名存入该实例,并将一些附加信息作为键值,一起存放在 WeakMap 里面。这时,WeakMap 里面对`element`的引用就是弱引用,不会被计入垃圾回收机制。 <br> 也就是说,DOM 节点对象的引用计数是`1`,而不是`2`。这时,一旦消除对该节点的引用,它占用的内存就会被垃圾回收机制释放。Weakmap 保存的这个键值对,也会自动消失。 <br> 基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。 <br> <br> ## WeakMap 示例 WeakMap 的例子很难演示,因为无法观察它里面的引用会自动消失。此时,其他引用都解除了,已经没有引用指向 WeakMap 的键名了,导致无法证实那个键名是不是存在。 <br> 首先,打开 Node 命令行。 ~~~ node --expose-gc ~~~ <br> 上面代码中,`--expose-gc`参数表示允许手动执行垃圾回收机制。 <br> 然后,执行下面的代码。 ~~~ // 手动执行一次垃圾回收,保证获取的内存使用状态准确 > global.gc(); undefined // 查看内存占用的初始状态,heapUsed 为 4M 左右 > process.memoryUsage(); { rss: 21106688, heapTotal: 7376896, heapUsed: 4153936, external: 9059 } > let wm = new WeakMap(); undefined > let b = new Object(); undefined > global.gc(); undefined // 此时,heapUsed 仍然为 4M 左右 > process.memoryUsage(); { rss: 20537344, heapTotal: 9474048, heapUsed: 3967272, external: 8993 } // 在 WeakMap 中添加一个键值对, // 键名为对象 b,键值为一个 5*1024*1024 的数组 > wm.set(b, new Array(5*1024*1024)); WeakMap {} // 手动执行一次垃圾回收 > global.gc(); undefined // 此时,heapUsed 为 45M 左右 > process.memoryUsage(); { rss: 62652416, heapTotal: 51437568, heapUsed: 45911664, external: 8951 } // 解除对象 b 的引用 > b = null; null // 再次执行垃圾回收 > global.gc(); undefined // 解除 b 的引用以后,heapUsed 变回 4M 左右 // 说明 WeakMap 中的那个长度为 5*1024*1024 的数组被销毁了 > process.memoryUsage(); { rss: 20639744, heapTotal: 8425472, heapUsed: 3979792, external: 8956 } ~~~ <br> 上面代码中,只要外部的引用消失,WeakMap 内部的引用,就会自动被垃圾回收清除。由此可见,有了它的帮助,解决内存泄漏就会简单很多。 <br> <br> # 参考资料 [JavaScript如何工作:内存管理+如何处理4个常见的内存泄漏](https://segmentfault.com/a/1190000017392370) [JavaScript 内存泄漏教程](http://www.ruanyifeng.com/blog/2017/04/memory-leak.html)