JS之内存泄露
JS之内存泄漏
从本质上说,内存泄漏可以定义为:不再被应用程序所需要的内存,出于某种原因,它不会返回到操作系统或空闲内存池中。
四种常见的内存泄漏
1.全局变量
JavaScript以一种有趣的方式处理未声明的变量: 对于未声明的变量,会在全局范围中创建一个新的变量来对其进行引用。在浏览器中,全局对象是window。例如:
function foo(args) { |
如果 bar 在 foo 函数的作用域内对一个变量进行引用,却忘记使用 var 来声明它,那么将创建一个意想不到的全局变量。
创建一个意料之外的全局变量的另一种方法是使用this:
function foo() { |
可以在JavaScript文件的开头通过添加“use strict”来避免这一切,它将开启一个更严格的JavaScript解析模式,以防止意外创建全局变量。
尽管我们讨论的是未知的全局变量,但仍然有很多代码充斥着显式的全局变量。根据定义,这些是不可收集的(除非被指定为空或重新分配)。用于临时存储和处理大量信息的全局变量特别令人担忧。如果你必须使用一个全局变量来存储大量数据,那么请确保将其指定为null,或者在完成后将其重新赋值。
2.被遗忘的定时器和回调
以setInterval
为例,因为它在JavaScript中经常使用。
var serverData = loadData(); |
上面的代码片段演示了使用定时器时引用不再需要的节点或数据。
renderer 表示的对象可能会在未来的某个时间点被删除,从而导致内部处理程序中的一整块代码都变得不再需要。但是,由于定时器仍然是活动的,所以,处理程序不能被收集,并且其依赖项也无法被收集。这意味着,存储着大量数据的 serverData 也不能被收集
作为开发者时,需要确保在完成它们之后进行显式删除它们(或者对象将无法访问)。
在过去,一些浏览器无法处理这些情况(很好的IE6)。幸运的是,现在大多数现代浏览器会为帮你完成这项工作:一旦观察到的对象变得不可访问,即使忘记删除事件监听器,它们也会自动收集观察者处理程序。然而,我们还是应该在对象被处理之前显式地删除这些观察者。例如:
var element = document.getElementById('launch-button'); |
如今,现在的浏览器(包括IE和Edge)使用现代的垃圾回收算法,可以立即发现并处理这些循环引用。换句话说,在一个节点删除之前也不是必须要调用removeEventListener。
3.闭包
闭包是javascript开发的一个关键方面,一个内部函数使用了外部(封闭)函数的变量。由于JavaScript运行的细节(静态作用域,作用域链),它可能以下面的方式造成内存泄漏:
下面这个例子相当于是一个闭包的递归引用
var theThing = null; |
每次调用replaceThing
的时候,theThing
都会得到一个包含一个大数组和一个新闭包(someMethod)的新对象。
同时,变量unused
指向一个引用了``originalThing`的闭包。
是不是有点困惑了? 重要的是,一旦具有相同父作用域的多个闭包的作用域被创建,则这个作用域就可以被共享。
在这种情况下,为闭包someMethod
而创建的作用域可以被unused
共享的。unused
内部存在一个对originalThing
的引用。即使unused
从未使用过,someMethod
也可以在replaceThing
的作用域之外(例如在全局范围内)通过theThing
来被调用。
由于someMethod
共享了unused
闭包的作用域,那么unused
引用包含的originalThing
会迫使它保持活动状态(两个闭包之间的整个共享作用域)。这阻止了它被收集。
当这段代码重复运行时,可以观察到内存使用在稳定增长,当GC
运行后,内存使用也不会变小。从本质上说,在运行过程中创建了一个闭包链表(它的根是以变量theThing
的形式存在),并且每个闭包的作用域都间接引用了了一个大数组,这造成了相当大的内存泄漏。
4.脱离DOM的引用
有时,将DOM节点存储在数据结构中可能会很有用。假设你希望快速地更新表中的几行内容,那么你可以在一个字典或数组中保存每个DOM行的引用。这样,同一个DOM元素就存在两个引用:一个在DOM树中,另一个则在字典中。如果在将来的某个时候你决定删除这些行,那么你需要将这两个引用都设置为不可访问。
var elements = { |
在引用 DOM 树中的内部节点或叶节点时,还需要考虑另外一个问题。如果在代码中保留对表单元格的引用(标记),并决定从 DOM 中删除表,同时保留对该特定单元格的引用,那么可能会出现内存泄漏。
你可能认为垃圾收集器将释放除该单元格之外的所有内容。然而,事实并非如此,由于单元格是表的一个子节点,而子节点保存对父节点的引用,所以对表单元格的这个引用将使整个表保持在内存中,所以在移除有被引用的节点时候要移除其子节点。
内存泄漏的识别方法
怎样可以观察到内存泄漏呢?
如果连续五次垃圾回收之后,内存占用一次比一次大,就有内存泄漏。这就要求实时查看内存占用。
浏览器
Chrome 浏览器查看内存占用,按照以下步骤操作
- 打开开发者工具,选择 Performance 面板
- 勾选 Memory
- 点击左上角的录制按钮。
- 在页面上进行各种操作,模拟用户的使用情况。
- 一段时间后,点击对话框的 stop 按钮,面板上就会显示这段时间的内存占用情况。
如果内存占用基本平稳,接近水平,就说明不存在内存泄漏。
反之,就是内存泄漏了。
命令行
命令行可以使用 Node 提供的process.memoryUsage
方法。
node –expose-gc
console.log(process.memoryUsage()); |
process.memoryUsage
返回一个对象,包含了 Node 进程的内存占用信息。该对象包含四个字段,单位是字节,含义如下。
- rss(resident set size):所有内存占用,包括指令区和堆栈。
- heapTotal:”堆”占用的内存,包括用到的和没用到的。
- heapUsed:用到的堆的部分。
- external: V8 引擎内部的 C++ 对象占用的内存。