【翻译】JavaScript内存泄露
原文地址:http://javascript.info/tutorial/memory-leaks#tools
我们在进行JavaScript开发时,很少会考虑内存的管理。JavaScript中变量的声明和使用看起来是一件很轻松的事,底层的细节处理交给浏览器去做就好了。
但是,随着web应用变得越来越庞大以及AJAX的使用,用户在一个网页中操作和停留的时间越来越久,我们会注意到浏览器占用的内存越来越大甚至到达了G数量级。造成这个问题的罪魁祸首就是memory leak(内存泄露)。
下面我们将讨论一下内存的管理以及最常见的内存泄露问题。
JavaScript的内存管理
JavaScript内存管理的核心概念:可达性(reachability)
- 所谓可达性指的是一些可被全局作用域访问到的对象(原文:A distinguished set of objects are assumed to be reachable: these are known as the roots)。最典型的的便是在调用栈(call stack)中被引用的全部对象(即当前function域内的所有局部变量和参数),以及部分全局变量。
- 只有一个对象可以被全局作用域直接访问或通过一系列的引用链间接访问,那么这个对象便会一直占用内存。
译者注:翻译的不太满意,原文讲的也略为晦涩。个人理解为所谓可达性这样理解:如果一个对象obj可以通过window.obj访问到,或者window.a.b.c.obj(其中a,b,c泛指层层作用域)访问到,那么便可以将这个对象obj是可达的。
浏览器的垃圾回收器会自动处理不可达的对象。
举个栗子:
function Menu(title) {
this.title = title
this.elem = document.getElementById('id')
}
var menu = new Menu('My Menu')
document.body.innerHTML = '' // (1)
menu = new Menu('His menu') // (2)
上述代码的内存结构如下图所示:

在注释(1)处,body.innerHTML被清除,理论上讲body的所有子节点被清除了,因为它们无法再被访问到。
但是通过document.getElementById('id')获取的这个节点是个例外,我们可以通过menu.elem访问到它,所以它仍然占用着内存。当然,如果此时检查它的父节点parentNode将会返回null。
也就是说:父节点被清空并不能保证其子节点全部被清除。
在注释(2)处,window.menu的引用被重新定义,使得原来的menu无法被访问。
这种情况下,原来的menu会被浏览器的垃圾回收器处理掉。

此时,整个旧menu结构被彻底删除。当然,如果存在与它关联的其他代码,则它仍然保持完整。
译者注:也就是说,如果代码中存在与旧menu作用域关联的对象,即使重新定义window.menu的引用对象,旧的menu结构仍然不会被回收。
循环引用-Circular references collection
闭包经常会引起循环引用,举个栗子:
function setHandler() {
var elem = document.getElementById('id')
elem.onclick = function() {
// ...
}
}
此例中,DOM节点elem通过onclick直接引用一个匿名function,同时,这个匿名function引用其外层作用域setHandler的elem(典型的闭包)。

译者注:上图中的LexicalEnvironment指的是setHandler的作用域。
通过这种内存结构可以得出结论:即使handler内没有任何代码,一些特殊的方法比如addEventListener/attachEvent也能够从其内部创建相关引用。
监听器handler一般伴随着相关DOM节点同时被清除:
function cleanUp() {
var elem = document.getElementById('id')
elem.parentNode.removeChild(elem)
}
调用cleanUp()删除DOM节点elem,此时cleanUp函数作用域仍然存在elem的引用LexialEnvironment.elem,但是因为没有嵌套的函数与之关联作用域,所以cleanUp的LexialEnvironment被回收。然后,elem便不具备可达性,其handler也随之被回收。
内存泄露
内存泄露指的是浏览器因为种种原因没有回收无用对象占用的内存。
内存泄露的原因可能是浏览器的bug,或者浏览器扩展插件的问题,但是更多的时候,是因为我们代码结构的不严谨。
IE8以下浏览器的DOM-JS内存泄露
IE8版本以前的浏览器不能够回收DOM对象和JavaScript之间的循环引用。
IE6的SP3版本问题更严重,甚至网页关闭以后仍然不能回收内存。
所以,上文提到的setHandler在IE8以下浏览器中,elem以及其关联的闭包never被回收。
function setHandler() {
var elem = document.getElementById('id')
elem.onclick = function() { /* ... */ }
}
除了DOM节点,其他对象(比如XHR)也会引起问题。
我们可以通过以下代码打破IE浏览器的循环引用。

将elem=null,从而监听器handler无法引用此DOM节点,这样便破坏了循环引用。
这种解决方法虽然存在弊端,但对付IE浏览器却也不失为一种好对策。
对于IE浏览器的内存泄露问题,读者可以参考Understanding and Solving Internet Explorer Leak Patterns和Circular Memory Leak Mitigation
XmlHttpRequest的内存管理与内存泄露
在IE9以下版本浏览器运行如下代码:
var xhr = new XMLHttpRequest() // or ActiveX in older IE
xhr.open('GET', '/server.url', true)
xhr.onreadystatechange = function() {
if(xhr.readyState == 4 && xhr.status == 200) {
// ...
}
}
xhr.send(null)
每次运行的内存结构如下:

异步对象XHR会被浏览器跟踪,产生一个对它的内部引用。
理论上讲,每次请求完成后,XHR对象的引用就会被清除。但是IE9一下版本的浏览器并不会这么做。
请在IE9以下版本访问此demo
幸运的是,我们可以轻松地解决这个问题:在闭包内删除xhr对象,在handler内部通过this访问它。
var xhr = new XMLHttpRequest()
xhr.open('GET', 'jquery.js', true)
xhr.onreadystatechange = function() {
if(this.readyState == 4 && this.status == 200) {
document.getElementById('test').innerHTML++
}
}
xhr.send(null)
xhr = null
}, 50)

这样便破坏了循环引用,解决了内存泄露问题。IE9以下版本打开demo
setInterval/setTimeout
setInterval/setTimeout使用的函数同样存在内部引用并且被浏览器跟踪直到运行结束,随后被回收。
对于setInterval,通过clearInterval来结束运行,但是setInterval运行的函数如果存在跨域引用,也会引起内存泄露。
对于服务器端的JS和V8引擎关于setInterval的问题可以参考:Memory leak when running setInterval in a new context
内存泄露的占用空间
简单的数据结构引起的内存泄露所占用的空间很少。
但是,因为闭包机制,如果关联外层作用域的内层函数保持活跃,外层的所有变量也会被保留。
想象一下,如果一个函数中存在一个庞大的字符串变量:
function f() {
var data = "Large piece of data, probably received from server"
/* do something using data */
function inner() {
// ...
}
return inner
}
只要inner函数保持活跃,外层的f函数作用域就不会被回收,变量data将会占用内存资源。
JavaScript解释器无法判断哪个外层变量被内层函数引用,所以它选择保留外层的所有变量。我希望最新的解释器可以针对这个问题进行优化,但难以预料它是否能够办到
事实上,这样的机制也是有好处的,很多情况下并不算是内层泄露。比如每次请求创建的函数,它们不被回收因为它们是监听器或者其他有用的东西。
如果外层的变量只被外层函数使用而不被内层函数引用,这样的变量可以通过设置null来节省内存。
function f() {
var data = "Large piece of data, probably received from server"
/* do something using data */
function inner() {
// ...
}
data = null
return inner
}
此时,虽然变量data仍然作为外层函数的一个属性占用着内存,但相比较庞大的字符串,它占用的资源空间大幅减少。
jQuery内存泄露处理方法及其弊端
jQuery用$.data方法处理IE6-7的内存泄露,不幸的是,与此同时也引起了jQuery专属的泄露问题。
$.data函数的本质是将JavaScript实体与DOM节点绑定,然后通过DOM节点来进行对JavaScript实体的读/存操作:
$(document.body).data('prop', 'val') // set
alert( $(document.body).data('prop') ) // get
$(elem).data(prop, val)的工作原理如下:
- 首先为本DOM节点分配一个唯一的数字标识(如果之前没有设置过):
elem[ jQuery.expando ] = id = ++jQuery.uuid // from jQuery source
其中jQuery.expando是一个随机数,不会发生重复。
2. 待设置的属性被赋予一个特殊的对象jQuery.cache
jQuery.cache[id]['prop'] = val
当需要读取DOM节点的data属性时,原理如下:
- DOM节点的唯一数字标识被重新获取
id = elem[ jQuery.expando ]; - data属性通过
jQuery.cache[id]获取。
这个API的目的是令DOM节点不产生对JavaScript对象的直接引用。用一个安全的数字来标识。被设置的data属性在jQuery.cache中,内部的事件监听也是通过$.data()API驱动。
但是这样做有一个严重的副作用:被设置data属性的元素不能通过原生代码删除。
举个栗子:
$('<div/>')
.html(new Array(1000).join('text')) // div with a text, maybe AJAX-loaded
.click(function() { })
.appendTo('#data')
document.getElementById('data').innerHTML = ''
子节点伴随着父节点innerHTML=''被清除,但是被设置的data属性仍然保留在jQuery.cache中,更重要的是,此节点对应的事件监听器也被保留下来,最终结果就是:此节点与它的监听器,和整个闭包,都被保留下来,引起内存泄露。
再举个稍微简单的栗子:
function go() {
$('<div/>')
.html(new Array(1000).join('text'))
.click(function() { })
}
解决方法
首先,应该使用jQuery API删除元素,如remove(),empty()和html(),这些方法可以查找后裔节点的data属性并删除它们。这样做可能有点过头,但起码保证了清空了被占用的内存。
如果对性能要求比较严格,解决方法还是很纠结的。
- 如果你很清楚地了解哪个元素存在handler并且它们的数量不多,可以安全的使用
removeData()手动清除data属性。然后就可以使用detach()方法了,detach()方法在删除元素的同时并不会清除data属性和原生方法; - 如果你不喜欢第一种方法,并且DOM树非常庞大,你可以将
$elem.detach()和$(elem).remove()通过setTimteout执行,通过异步执行来避免视觉卡顿(evade visual sluggishnes)。
检查jQuery的内存泄露非常简单,查看$.cache可以很方便的找出问题的引发原因。
jQuery的问题讨论到此为止。
找出并修复问题
找出问题
内存泄露的方式有很多,浏览器也不断有新的bug出现。我们甚至会发现HTML5中存在功能性的泄露,为了修复它们,首先我们需要重现它们并找出解决方案。
浏览器并不会立即执行内存清除工作,许多垃圾回收器算法都是不定时地清理内存。浏览器也可能等待达到一定的限定值时再执行清理工作。
所以,如果你发现了内存泄露问题,或许你需要等待一段时间才能执行回收操作。
浏览器占用的内存可能会越来越多,但最终在一段时间之后它会进行清理工作。
Don’t take a minute of increasing memory as a loop proof if the overall footprint is still low. Add something to leaking objects. A large string will do.(这段不会翻)
准备浏览器
与网页有交互的浏览器第三方扩展可能会引起内存泄露,所以首先需要保证:
- 禁用Flash;
- 禁用杀毒软件以及与浏览器有交互的其他软件;
- 禁用插件。
IE可以通过设置命令行参数禁用插件
"C:\Program Files\Internet Explorer\iexplore.exe" -extoff
也可以在浏览器中设置:


Firefox可以通过下述命令来运行profile管理区创建一个空的profile:
firefox --profilemanager

【翻译】JavaScript内存泄露的更多相关文章
- 4类 JavaScript 内存泄露及如何避免
原文:4 Types of Memory Leaks in JavaScript and How to Get Rid Of Them笔记:涂鸦码龙 译者注:本文并没有逐字逐句的翻译,而是把我认为重要 ...
- JavaScript内存泄露,闭包内存泄露如何解决
本文原链接:https://cloud.tencent.com/developer/article/1340979 JavaScript 内存泄露的4种方式及如何避免 简介 什么是内存泄露? Java ...
- [ Javascript ] 内存泄露以及循环引用解析
内存泄露 在javascript中,我们非常少去关注内存的管理. 我们创建变量,使用变量,浏览器关注这些底层的细节都显得非常正常. 可是当应用程序变得越来越复杂而且ajax化之后,或者用户在一个页面停 ...
- 常见的几种JavaScript内存泄露
总结一下常见的几种JavaScript内存泄露: 1.意外的全局变量 全局变量属于window对象,所以只会随着window销毁才会销毁. 2.console.log() conaole.log()函 ...
- Javascript内存泄露
在过去一些的时候,Web开发人员并没有太多的去关注内存泄露问题.那时的页面间联系大都比较简单,并主要使用不同的连接地址在同一个站点中导航,这样的设计方式是非常有利于浏览器释放资源的.即使Web页面运行 ...
- (译)JavaScript内存泄露
译者前言 原文地址:Memory leaks 最近简单了解了下JavaScript的闭包和垃圾回收机制(GC),这中间也不得不接触内存泄露这个概念.然后不小心找到了这篇文章,看下来后理解了不少东西,于 ...
- [转]常见的JavaScript内存泄露
什么是内存泄露 内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存.内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制, ...
- JavaScript 内存泄露以及如何处理
一.前言 一直有打算总结一下JS内存泄露的方面的知识的想法,但是总是懒得提笔. 富兰克林曾经说过:懒惰,像生鏽一样,比操劳更能消耗身体,经常用的钥匙总是亮闪闪的.安利一下,先起个头. 二.内存声明周期 ...
- 常见的 JavaScript 内存泄露
什么是内存泄露 指由于疏忽或错误造成程序未能释放已经不再使用的内存.内存泄漏并非指内存在物理上的消失, 而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造 ...
随机推荐
- linux '--stdin'错误 -批量修改密码
虚拟机:VMware虚拟机 系统:Linux ubuntu 4.4.0-31-generic #50~14.04.1-Ubuntu SMP Wed Jul 13 01:07:32 UTC 2016 ...
- node.js 使用 superagent 与 cheerio 完成简单爬虫
目标 建立一个 lesson3 项目,在其中编写代码. 当在浏览器中访问 http://localhost:3000/ 时,输出 CNode(https://cnodejs.org/ ) 社区首页的所 ...
- 图解HTTP第十章
Web 的攻击技术 1>在客户端即可篡改请求 2>针对 Web 应用的攻击模式 3>因输出值转义不完全引发的安全漏洞 [1]跨站脚本攻击 [2]SQL 注入攻击 [3]OS 命令注入 ...
- python 基础———— 字符串常用的调用 (图2)
1. replace 2. join 3.split 4 rsplit 5. strip : 去除字符串左右两边特定(指定)的字符 7. rstrip : 去除右边特定(指定)的字符 8. l ...
- mysql 的 alter table 操作性能小提示
通常情况下,修改表的结构一般不会有太大问题,无非就是一个 alter table 操作,但是对于大表做 alter 操作是一个大问题,请小伙伴们慎重. mysql执行大部分修改表结构操作方法是创建一个 ...
- 周报数据采集之生存图片(execl方法)
https://blog.csdn.net/Luzaofa/article/details/81675364 Python之Excel chart另存为图片大家好,好久没有更新博客了,这一段时间有点忙 ...
- css 文本超出2行就隐藏并且显示省略号
overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-box-orient: vertical; -webk ...
- 文件上传的三种模式-Java
文件上传的三种方式-Java 前言:因自己负责的项目(jetty内嵌启动的SpringMvc)中需要实现文件上传,而自己对java文件上传这一块未接触过,且对 Http 协议较模糊,故这次采用渐进的方 ...
- linux系统中使用socket直接发送ARP数据
这个重点是如这样创建socket: sock_send = socket ( PF_PACKET , SOCK_PACKET , htons ( ETH_P_ARP) ) ; 其后所有收发的数据都是 ...
- Python mayavi库及mayavi管线
Mayavi库的基本元素Mayavi库中主要有两大部分功能 一类是用于处理图形可视化和图形操作的mlab模块 一类是操作管线对象窗口对象的apimlab包含 绘图函数:实现已有的数据进行可视化显示,可 ...