原文:4 Types of Memory Leaks in JavaScript and How to Get Rid Of Them
笔记:涂鸦码龙javascript译者注:本文并无逐字逐句的翻译,而是把我认为重要的信息作了翻译。若是您的英文熟练,能够直接阅读原文。java
本文将探索常见的客户端 JavaScript 内存泄露,以及如何使用 Chrome 开发工具发现问题。node
内存泄露是每一个开发者最终都要面对的问题,它是许多问题的根源:反应迟缓,崩溃,高延迟,以及其余应用问题。git
本质上,内存泄露能够定义为:应用程序再也不须要占用内存的时候,因为某些缘由,内存没有被操做系统或可用内存池回收。编程语言管理内存的方式各不相同。只有开发者最清楚哪些内存不须要了,操做系统能够回收。一些编程语言提供了语言特性,能够帮助开发者作此类事情。另外一些则寄但愿于开发者对内存是否须要清晰明了。github
JavaScript 是一种垃圾回收语言。垃圾回收语言经过周期性地检查先前分配的内存是否可达,帮助开发者管理内存。换言之,垃圾回收语言减轻了“内存仍可用”及“内存仍可达”的问题。二者的区别是微妙而重要的:仅有开发者了解哪些内存在未来仍会使用,而不可达内存经过算法肯定和标记,适时被操做系统回收。算法
垃圾回收语言的内存泄露主因是不须要的引用。理解它以前,还需了解垃圾回收语言如何辨别内存的可达与不可达。chrome
大部分垃圾回收语言用的算法称之为 Mark-and-sweep 。算法由如下几步组成:编程
现代的垃圾回收器改良了算法,可是本质是相同的:可达内存被标记,其他的被看成垃圾回收。数组
不须要的引用是指开发者明知内存引用再也不须要,却因为某些缘由,它仍被留在激活的 root 树中。在 JavaScript 中,不须要的引用是保留在代码中的变量,它再也不须要,却指向一块本该被释放的内存。有些人认为这是开发者的错误。浏览器
为了理解 JavaScript 中最多见的内存泄露,咱们须要了解哪一种方式的引用容易被遗忘。
JavaScript 处理未定义变量的方式比较宽松:未定义的变量会在全局对象建立一个新变量。在浏览器中,全局对象是 window
。
1 |
function foo(arg) { |
真相是:
1 |
function foo(arg) { |
函数 foo
内部忘记使用 var
,意外建立了一个全局变量。此例泄露了一个简单的字符串,无伤大雅,可是有更糟的状况。
另外一种意外的全局变量可能由 this
建立:
1 |
function foo() { |
在 JavaScript 文件头部加上
'use strict'
,能够避免此类错误发生。启用严格模式解析 JavaScript ,避免意外的全局变量。
全局变量注意事项
尽管咱们讨论了一些意外的全局变量,可是仍有一些明确的全局变量产生的垃圾。它们被定义为不可回收(除非定义为空或从新分配)。尤为当全局变量用于临时存储和处理大量信息时,须要多加当心。若是必须使用全局变量存储大量数据时,确保用完之后把它设置为 null 或者从新定义。与全局变量相关的增长内存消耗的一个主因是缓存。缓存数据是为了重用,缓存必须有一个大小上限才有用。高内存消耗致使缓存突破上限,由于缓存内容没法被回收。
在 JavaScript 中使用 setInterval
很是日常。一段常见的代码:
1 |
var someResource = getData(); |
此例说明了什么:与节点或数据关联的计时器再也不须要,node
对象能够删除,整个回调函数也不须要了。但是,计时器回调函数仍然没被回收(计时器中止才会被回收)。同时,someResource
若是存储了大量的数据,也是没法被回收的。
对于观察者的例子,一旦它们再也不须要(或者关联的对象变成不可达),明确地移除它们很是重要。老的 IE 6 是没法处理循环引用的。现在,即便没有明确移除它们,一旦观察者对象变成不可达,大部分浏览器是能够回收观察者处理函数的。
观察者代码示例:
1 |
var element = document.getElementById('button'); |
对象观察者和循环引用注意事项
老版本的 IE 是没法检测 DOM 节点与 JavaScript 代码之间的循环引用,会致使内存泄露。现在,现代的浏览器(包括 IE 和 Microsoft Edge)使用了更先进的垃圾回收算法,已经能够正确检测和处理循环引用了。换言之,回收节点内存时,没必要非要调用 removeEventListener
了。
有时,保存 DOM 节点内部数据结构颇有用。假如你想快速更新表格的几行内容,把每一行 DOM 存成字典(JSON 键值对)或者数组颇有意义。此时,一样的 DOM 元素存在两个引用:一个在 DOM 树中,另外一个在字典中。未来你决定删除这些行时,须要把两个引用都清除。
1 |
var elements = { |
此外还要考虑 DOM 树内部或子节点的引用问题。假如你的 JavaScript 代码中保存了表格某一个 <td>
的引用。未来决定删除整个表格的时候,直觉认为 GC 会回收除了已保存的 <td>
之外的其它节点。实际状况并不是如此:此 <td>
是表格的子节点,子元素与父元素是引用关系。因为代码保留了 <td>
的引用,致使整个表格仍待在内存中。保存 DOM 元素引用的时候,要当心谨慎。
闭包是 JavaScript 开发的一个关键方面:匿名函数能够访问父级做用域的变量。
代码示例:
1 |
var theThing = null; |
代码片断作了一件事情:每次调用 replaceThing
,theThing
获得一个包含一个大数组和一个新闭包(someMethod
)的新对象。同时,变量 unused
是一个引用 originalThing
的闭包(先前的 replaceThing
又调用了 theThing
)。思绪混乱了吗?最重要的事情是,闭包的做用域一旦建立,它们有一样的父级做用域,做用域是共享的。someMethod
能够经过 theThing
使用,someMethod
与 unused
分享闭包做用域,尽管 unused
从未使用,它引用的 originalThing
迫使它保留在内存中(防止被回收)。当这段代码反复运行,就会看到内存占用不断上升,垃圾回收器(GC)并没有法下降内存占用。本质上,闭包的链表已经建立,每个闭包做用域携带一个指向大数组的间接的引用,形成严重的内存泄露。
Meteor 的博文 解释了如何修复此种问题。在
replaceThing
的最后添加originalThing = null
。
Chrome 提供了一套很棒的检测 JavaScript 内存占用的工具。与内存相关的两个重要的工具:timeline
和 profiles
。
timeline 能够检测代码中不须要的内存。在此截图中,咱们能够看到潜在的泄露对象稳定的增加,数据采集快结束时,内存占用明显高于采集初期,Node(节点)的总量也很高。种种迹象代表,代码中存在 DOM 节点泄露的状况。
Profiles 是你能够花费大量时间关注的工具,它能够保存快照,对比 JavaScript 代码内存使用的不一样快照,也能够记录时间分配。每一次结果包含不一样类型的列表,与内存泄露相关的有 summary(概要) 列表和 comparison(对照) 列表。
summary(概要) 列表展现了不一样类型对象的分配及合计大小:shallow size(特定类型的全部对象的总大小),retained size(shallow size 加上其它与此关联的对象大小)。它还提供了一个概念,一个对象与关联的 GC root 的距离。
对比不一样的快照的 comparison list 能够发现内存泄露。
实质上有两种类型的泄露:周期性的内存增加致使的泄露,以及偶现的内存泄露。显而易见,周期性的内存泄露很容易发现;偶现的泄露比较棘手,通常容易被忽视,偶尔发生一次可能被认为是优化问题,周期性发生的则被认为是必须解决的 bug。
以 Chrome 文档中的代码为例:
1 |
var x = []; |
当 grow
执行的时候,开始建立 div 节点并插入到 DOM 中,而且给全局变量分配一个巨大的数组。经过以上提到的工具能够检测到内存稳定上升。
timeline 标签擅长作这些。在 Chrome 中打开例子,打开 Dev Tools ,切换到 timeline,勾选 memory 并点击记录按钮,而后点击页面上的 The Button
按钮。过一阵中止记录看结果:
两种迹象显示出现了内存泄露,图中的 Nodes(绿线)和 JS heap(蓝线)。Nodes 稳定增加,并未降低,这是个显著的信号。
JS heap 的内存占用也是稳定增加。因为垃圾收集器的影响,并不那么容易发现。图中显示内存占用忽涨忽跌,实际上每一次下跌以后,JS heap 的大小都比原先大了。换言之,尽管垃圾收集器不断的收集内存,内存仍是周期性的泄露了。
肯定存在内存泄露以后,咱们找找根源所在。
切换到 Chrome Dev Tools 的 profiles 标签,刷新页面,等页面刷新完成以后,点击 Take Heap Snapshot 保存快照做为基准。然后再次点击 The Button
按钮,等数秒之后,保存第二个快照。
筛选菜单选择 Summary,右侧选择 Objects allocated between Snapshot 1 and Snapshot 2,或者筛选菜单选择 Comparison ,而后能够看到一个对比列表。
此例很容易找到内存泄露,看下 (string)
的 Size Delta
Constructor,8MB,58个新对象。新对象被分配,可是没有释放,占用了8MB。
若是展开 (string)
Constructor,会看到许多单独的内存分配。选择某一个单独的分配,下面的 retainers 会吸引咱们的注意。
咱们已选择的分配是数组的一部分,数组关联到 window
对象的 x
变量。这里展现了从巨大对象到没法回收的 root(window
)的完整路径。咱们已经找到了潜在的泄露以及它的出处。
咱们的例子还算简单,只泄露了少许的 DOM 节点,利用以上提到的快照很容易发现。对于更大型的网站,Chrome 还提供了 Record Heap Allocations 功能。
回到 Chrome Dev Tools 的 profiles 标签,点击 Record Heap Allocations。工具运行的时候,注意顶部的蓝条,表明了内存分配,每一秒有大量的内存分配。运行几秒之后中止。
上图中能够看到工具的杀手锏:选择某一条时间线,能够看到这个时间段的内存分配状况。尽量选择接近峰值的时间线,下面的列表仅显示了三种 constructor:其一是泄露最严重的(string)
,下一个是关联的 DOM 分配,最后一个是 Text
constructor(DOM 叶子节点包含的文本)。
从列表中选择一个 HTMLDivElement
constructor,而后选择 Allocation stack
。
如今知道元素被分配到哪里了吧(grow
-> createSomeNodes
),仔细观察一下图中的时间线,发现 HTMLDivElement
constructor 调用了许屡次,意味着内存一直被占用,没法被 GC 回收,咱们知道了这些对象被分配的确切位置(createSomeNodes
)。回到代码自己,探讨下如何修复内存泄露吧。
在 heap allocations 的结果区域,选择 Allocation。
这个视图呈现了内存分配相关的功能列表,咱们马上看到了 grow
和 createSomeNodes
。当选择 grow
时,看看相关的 object constructor,清楚地看到 (string)
, HTMLDivElement
和 Text
泄露了。
结合以上提到的工具,能够轻松找到内存泄露。
附件jpg 改 rar