【翻译】 内存泄漏



  这篇文章是我在逛微博的时候,@清-三水清 推荐的一篇文章,今天有空看了收益很大,就翻译了出来。
  这是我第一次翻译,而且本身英语就不是很好,所以难免会有错误。请指出。同时,求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(elem)
4 }
  调用cleanUp()从DOM中删除这个元素。虽然在词法环境里面还有LexialEnvironment.elem这个引用,但是没有内嵌函数,所以词法环境本身也是可以回收的。当词法环境可以被回收,那么elem本身也是不可访问了,将和handler一起被回收。

  
  
  内存泄露
  
  当浏览器因为某些原因没法释放无用对象的内存时,会发生内存泄露。
  这些原因包括浏览器的bug、浏览器的扩展问题、还有我们比较少见的代码结构问题。
  IE<8 DOM-JS 内存泄露
  
  IE8之前的版本都不能释放DOM对象与Javascript对象的循环引用。
  这个问题在IE6的SP3(mid-2007)这个版本更加严重,因为即使页面卸载(unload)了,内存还是不会释放。
  所以上面的代码setHandler在IE8以下版本都会泄露,elem元素和闭包永远不会被清除。
1 function setHandler() {
2 var elem = document.getElementById('id')
3 elem.onclick = function() { /* ... */ }
4 }

  IE内存泄露的解决发难是打断循环引用
  
  我们把elem指向null,所以handler不再引用DOM元素。这个循环链被破除。
  这种泄露虽然有很久历史了,但是一个很好的例子来说明如何破除循环链。

  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', true)
04
05 xhr.onreadystatechange = function() {
06 if(this.readyState == 4 && this.status == 200) {
07 document.getElementById('test').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这个函数需要哪些变量,所以它会把词法环境LexicalEnvironment)里面的所有东西保留下。我(作者)希望新的解析器能尝试去优化它,当然,这不一定成功。

  其实,这些变量有可能不是泄露。很多函数(这里作者指的是闭包)都是很理智地被创建的。比如,不去清理每个请求,因为这个的确需要被保存。

  假如,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', 'val') // 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].
  这个API的目的是DOM元素永远不会和JS对象有直接的引用。它只拥有一个编号就够了。数据都放在JQuery.cache里面。JQuery的事件监听模块也是在内部使用了$.data的API。(注:KISSY也是)

  但是这是有副作用的,不能使用DOM的原生方法去删除一个元素。
  例子:
  下面的代码在所有浏览器都会内存泄露:
1 $('<div/>')
2 .html(new Array(1000).join('text')) // div with a text, maybe AJAX-loaded
3 .click(function() { })
4 .appendTo('#data')
5
6 document.getElementById('data').innerHTML = ''
  因为DOM元素div已经通过清空父元素innerHTML“删除”了,但是div的属性(data)还留在jQuery.cache里面。更严重的,(cache中的)事件处理函数还保持对该div的引用,所以div并没有真正被删除,还保留在内存中。

  一个简单的内存泄露的例子:

  下面的代码会泄露
1 function go() {
2 $('<div/>')
3 .html(new Array(1000).join('text'))
4 .click(function() { })
5 }
  (注:我怀疑作者写多了行代码,应该没有第4行的,否者和上个例子没区别了)
  这里的问题在于 ,元素div被创建了,但没有被其他地方引用,所以函数执行完后,没有对这个div引用了。但是它的jQuery.cache 里面的data会还永久存在。
  逃开泄露

  首先,我们应该使用jQuery的API去删除元素。
  remove(), empty()html()些方法会检查元素包括它的子元素的data并清除它们。这会带来一些性能上得开销,但是至少内存剩下了。
  其实,如果对性能的要求很高的话,可以做点调整。
  1.首先,如果你知道每个哪个元素有绑定事件的,你可以用 removeData()手动清除它们的数据,这样是安全的。
  然后用 detach()这个函数(在清除元素的时候)不会清除数据或原生方法。
  2.如果你不喜欢上面的方法,而且用$elem.detach() ,然后把$(elem).remove()放入setTimeout里面,这样它会被异步删除,而且我们眼不见为净。

  还好,寻找到jQuery的内存泄露比较容易,检查$.cache的大小,如果大了,检查它里面的data所对应的对象是否存在。

  寻找内存泄露并且修复它
  浏览器有很多bug,新的浏览器bug还会出现,因为写代码本身就很难。
  所以我们也许会在HTML5的某个功能或者其他地方里面发现泄漏了。要修复泄露,我们首先需要尝试隔离(isolate )和再现(reproduce )它。
  浏览器不是马上清除内存的,大多数垃圾回收算法会偶尔清理一次。浏览器也许会延迟清理内存,直到超过了内存量一定限制才清除。
  所以你认为你在一个循环结构中找到了一个内存‘泄露’的问题,先等一会。
  浏览器会变大,但会随后或者超过一定值的时候,会释放内存。
  不要在整体内存量还小得时候,用一个一分钟的循环去增加内存量来证明内存泄露。增加点东西进去,例如一个长字符串就够了。
  准备好浏览器
  泄露有可能是因为浏览器的扩展在与页面交互。特别是两个扩展的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,我们可以看到有什么在里面。还可以和其他的进行比较

  
  大多数情况下,我们获得不了任何星系,但至少我们可以看到哪些对象残留了,和泄露对象大概的结构。
  内存泄露非常难控制,是一件你需要与之斗争的事情。
  








posted @ 2011-09-06 19:37  xiiiiiin  阅读(1316)  评论(1编辑  收藏  举报