【翻译】 内存泄漏
这篇文章是我在逛微博的时候,@清-三水清 推荐的一篇文章,今天有空看了收益很大,就翻译了出来。
这是我第一次翻译,而且本身英语就不是很好,所以难免会有错误。请指出。同时,求linux下的博客客户端。排个版太坑爹了。
译文如下:
在JS中,我们很少考虑内存管理。我们很自然地创建使用变量,然后让浏览器来负责底层的细节。
但是随着程序变得复杂、Ajax的出现,用户在页面停留的时间变长,我们发现了浏览器居然会占用了IG以上的内存,而且会越来越大。这其实是因为内存泄露了。
我们下面讨论最常出现的内存泄露以及对内存的管理。
JS中的内存管理
JS内存管理的核心概念是一个可访问性的概念(注:reachability 不懂怎么翻译)。
1.有一个对象集合很明显是可访问(reachable)的:它们被称作根对象(roots,注:这个不好翻译)。一般来说,它们(roots)包括在调用堆栈(call stack)任何地方都能引用到的对象(也就是,所有局部变量,和正在运行的函数中定义的参数),还包括全局变量。
2.一些因为被根对象引用到,或者被原型链引用的对象,它们会被保存在内存中,也是可访问的。
垃圾回收例子
看下下面代码来观察它是怎么工作的:
01 |
function Menu(title) { |
02 |
this .title = title |
03 |
this .elem = document.getElementById( 'id' ) |
04 |
} |
05 |
06 |
var menu = new Menu( 'My Menu' ) |
07 |
08 |
document.body.innerHTML = '' // (1) |
09 |
10 |
menu = new Menu( 'His menu' ) // (2) |
这里是它的内存结构:
在步骤(1)里面,body.innerHTML 被清空了。按理说,它们的子元素被删除了,因为它们不能被访问了。
……但是,#id这个元素并没有被删除,因为它还可以通过menu.elem被访问到,所以,它会留来内存里面。当然,你会发现它的parentNode会是null。
独立的DOM元素可能会残留在内存中,即使它们的父节点被清除了。
在步骤(2),window.memu被重新引用到别的地方(another Menu)了,所以,原来旧的引用(old menu)变成不可访问了。它会自动被浏览器的垃圾回收删除掉。
现在整个menu的结构(old menu)就被删除了,包括它本身。当然,如果我们代码中仍有其他对这个元素的引用,那它会留在内存。
循环引用回收
闭包会经常导致循环应用。例子:
1 |
function setHandler() { |
2 |
3 |
var elem = document.getElementById( 'id' ) |
4 |
5 |
elem.onclick = function () { |
6 |
// ... |
7 |
} |
8 |
9 |
} |
这里DOM元素引用了函数直接通过onclick,而且函数通过外部词法环境(LexicalEnvironment)也引用了这个DOM元素。
即使handler里面没有代码,也会出现这种内存结构。特殊的事件绑定方法,像addEventListener/attachEvent 也会在内部形成这样的引用结构。
我们通常这样销毁这个元素:
1 |
function cleanUp() { |
2 |
var elem = document.getElementById( 'id' ) |
3 |
elem.parentNode.removeChild( |
4 |
} |
调用cleanUp()从DOM中删除这个元素。虽然在词法环境里面还有LexialEnvironment.elem这个引用,但是没有内嵌函数,所以词法环境本身也是可以回收的。当词法环境可以被回收,那么elem本身也是不可访问了,将和handler一起被回收。
内存泄露
当浏览器因为某些原因没法释放无用对象的内存时,会发生内存泄露。
这些原因包括浏览器的bug、浏览器的扩展问题、还有我们比较少见的代码结构问题。
IE<8 DOM-JS 内存泄露
IE8之前的版本都不能释放DOM对象与Javascript对象的循环引用。
这个问题在IE6的SP3(mid-2007)这个版本更加严重,因为即使页面卸载(unload)了,内存还是不会释放。
1 |
function setHandler() { |
2 |
var elem = document.getElementById( 'id' ) |
3 |
elem.onclick = function () { /* ... */ } |
4 |
} |
IE内存泄露的解决发难是打断循环引用
我们把elem指向null,所以handler不再引用DOM元素。这个循环链被破除。
这种泄露虽然有很久历史了,但是一个很好的例子来说明如何破除循环链。
你可以得到更详细信息,看Understanding and Solving Internet Explorer Leak Patterns和 Circular Memory Leak Mitigation
XmlHttpRequest 的内存管理和泄露
下面的代码在IE9以下都泄露
01 |
var xhr = new XMLHttpRequest() // or ActiveX in older IE |
02 |
03 |
xhr.open( 'GET' , '/server.url' , true ) |
04 |
05 |
xhr.onreadystatechange = function () { |
06 |
if (xhr.readyState == 4 && xhr.status == 200) { |
07 |
// ... |
08 |
} |
09 |
} |
10 |
11 |
xhr.send( null ) |
我们来看下每一次运行的内存结构
异步的XMLHttpRequest对象是浏览器来控制(track)的,所以它们之间有一个内部的引用。
当请求结束的时候,这个引用被去除,所以xhr变得不可访问。但是再IE9以下不是这样的(注:作者指的是引用没有被去除)。
这里有个例子for IE<9 separate page example (注,这个例子挂了,因为作者代码写错了,用了XDomainRequest,很多IE老版本不支持)
幸运的是,解决这个问题很容易。我们需要从闭包中去掉对xhr的引用,通过this关键字来对它访问。
01 |
var xhr = new XMLHttpRequest() |
02 |
|
03 |
xhr.open( 'GET' , 'jquery.js' , t ) |
04 |
|
05 |
xhr.onreadystatechange = function () { |
06 |
if ( this .readyState == 4 && this .status == 200) { |
07 |
document.getElementById( ' ).innerHTML++ |
08 |
} |
09 |
} |
10 |
|
11 |
xhr.send( null ) |
12 |
xhr = null |
13 |
}, 50) |
这里有个例子 Sample page for IE (注:这个例子倒是好的,在XP IE8中,内存涨了100M左右就会有一次释放)
setTimeInterval/setTimeout
在setTimeInterval/setTimeout中使用的函数也互相内部引用,直至操作完成,然后被清除。
对setInterval来说,完成意味着clearInterval。即使函数根本什么都没做,但是这个循环(interval)并不清除,这会导致内存泄露。
服务端JS(server-side JS)和V8可以看这样的一个例子: Memory leak when running setInterval in a new context.
内存泄露量
普通的数据的内存泄露不会很大。
但是闭包创建了但很多长期存在的函数外部变量,只要内部函数不被清除就会一直存在。
所以想象下,你创建了一个这样的函数,而且它有一个变量,变量存储了一个很长的字符串。
01 |
function f() { |
02 |
var data = "Large piece of data, probably received from server" |
03 |
04 |
/* do something using data */ |
05 |
06 |
function inner() { |
07 |
// ... |
08 |
} |
09 |
10 |
return inner |
11 |
} |
当这个函数的inner函数存在内存里,然后一个有大变量的词法环境(
LexicalEnvironment)也会占着内存。直到inner这个函数挂了。
JavaScript解析器不会知道inner这个函数需要哪些LexicalEnvironmen
所有东西保留下。我(作者)
其实,这些变量有可能不是泄露。很多函数(这里作者指的是闭包)都是很理智地被创建的。比如, 不去清理每个请求,因为这个的确需要被保存。
假如,data这个变量只在外部的函数内使用(指的是f()),我们可以把它设null来节省内存。
01 |
function f() { |
02 |
var data = "Large piece of data, probably received from server" |
03 |
04 |
/* do something using data */ |
05 |
06 |
function inner() { |
07 |
// ... |
08 |
} |
09 |
10 |
data = null |
11 |
12 |
return inner |
13 |
} |
这样data的确还作为词法环境(LexicalEnvironment)的一个属性留在内存,但是它 没占了多少内存。
JQuery的内存泄露和防内存泄露的办法
JQurey 用 $.data API来对付IE6-7的内存泄露。非常不幸,它带来了一些只有JQuery特有的泄露。
$.data 的主要原则是,所有JS实例的属性都用以下JQuery语句来读写:
1 |
// works on this site cause it's using jQuery |
2 |
3 |
$(document.body).data( 'prop' ,
) // set |
4 |
alert( $(document.body).data( 'prop' ) ) // get |
JQuery中$(elem).data(prop, val) 语句做了这样的事情:(注:KISSY也差不多)
1. 如果元素还没有编号的话,为它赋予一个特定唯一的编号。
elem[ jQuery.expando ] = id = ++jQuery.uuid
// from jQuery source
2.data(要存储的数据)被放置在一个特别的对象里面jQuery.cache
jQuery.cache[id][
'prop'
] = val
当读取这个属性的时候:
1.得到元素唯一的编号
id = elem[ jQuery.expando ]
. 2.从JQuery.cache中读取数据
jQuery.cache[id]
. 但是这是有副作用的,不能使用DOM的原生方法去删除一个元素。
例子:
下面的代码在所有浏览器都会内存泄露:
1 |
$( '<div/>' ) |
2 |
.html( new Array(1000).join( ' )) // div with a text, maybe AJAX-loaded |
3 |
.click( function () { }) |
4 |
.appendTo( '#data' ) |
5 |
6 |
document.getElementById( 'data'
'' |
因为DOM元素div已经通过清空父元素innerHTML“删除”了,但是div的属性(data)还留在jQuery.cache里面。更严重的,(cache中的)事件处理函数还保持对该div的引用,所以div并没有真正被删除,还保留在内存中。
一个简单的内存泄露的例子:
下面的代码会泄露
1 |
function go() { |
2 |
$( '<div/>' ) |
3 |
.html( new Array(1000). 'text' )) |
4 |
.click( function () { }) |
5 |
} |
(注:我怀疑作者写多了行代码,应该没有第4行的,否者和上个例子没区别了)
这里的问题在于 ,元素div被创建了,但没有被其他地方引用,所以函数执行完后,没有对这个div引用了。但是它的jQuery.cache 里面的data会还永久存在。
逃开泄露
首先,我们应该使用jQuery的API去删除元素。
其实,如果对性能的要求很高的话,可以做点调整。
1.首先,如果你知道每个哪个元素有绑定事件的,你可以用 removeData() 手动清除它们的数据,这样是安全的。
然后用 detach() ,这个函数(在清除元素的时候)不会清除数据或原生方法。
2.如果你不喜欢上面的方法,而且用放入setTimeout里面,这样它会被异步删除,而且我们眼不见为净。
$elem.detach()
,然后把$(elem).remove() 还好,寻找到jQuery的内存泄露比较容易,检查$.cache的大小,如果大了,检查它里面的data所对应的对象是否存在。
寻找内存泄露并且修复它
浏览器有很多bug,新的浏览器bug还会出现,因为写代码本身就很难。
浏览器不是马上清除内存的,大多数垃圾回收算法会偶尔清理一次。浏览器也许会延迟清理内存,直到超过了内存量一定限制才清除。
所以你认为你在一个循环结构中找到了一个内存‘泄露’的问题,先等一会。
浏览器会变大,但会随后或者超过一定值的时候,会释放内存。
不要在整体内存量还小得时候,用一个一分钟的循环去增加内存量来证明内存泄露。增加点东西进去,例如一个长字符串就够了。
准备好浏览器
泄露有可能是因为浏览器的扩展在与页面交互。特别是两个扩展的bugs导致内存泄露。像Skype和antivirus的扩展,把它们都关了就没事了。
所以,准备步骤如下:
1.关flash
2.关杀毒什么的。包括和浏览器集成的Link checkers 等等。
3.关插件。所有插件。
对IE来说,有命令行可以做这样的工作:"C:\Program Files\Internet Explorer\iexplore.exe" -extoff
还要在IE里面关闭第三方的扩展
在FIREFOX下,用下面的命令来建一个干净的profile。
firefox --profilemanager
工具
在Chrome开发者工具里面,有Timeline-Memeory的选项卡
我们可以在 里面看到所占用的内存。
这里还有Profiles-Memory,我们可以看到有什么在里面。还可以和其他的进行比较
大多数情况下,我们获得不了任何星系,但至少我们可以看到哪些对象残留了,和泄露对象大概的结构。
内存泄露非常难控制,是一件你需要与之斗争的事情。
See also: