JavaScript内存泄漏

JavaScript内存泄漏

本文翻译自Memory leaks, by Ilya Kantor

在JavaScript中,我们很少考虑内存管理。我们创建变量,使用它们,并由浏览器去负责处理底层的细节,这看起来似乎挺自然的。

但是随着应用程序变得复杂,以及访客长时间在网页停留,我们可能会注意到一个浏览器需要占1G以上的内存,并且还不断的增长。这通常就是发生了内存泄漏。

在这我们将讨论内存管理和最常见的泄漏类型。

JavaScript的内存管理

JavaScript内存管理的中心思想是一个可达性的思想。

  1. 假定所有的显著的对象是可达的:这些被称为根。通常,这些包括从调用堆栈中的任何地方引用的所有对象(即,当前正在调用的函数中的所有局部变量和参数)和所有全局对象。
  2. 对象保存在内存中,而它们可以从根通过引用或引用链访问。

在浏览器中有一个可清除不可达对象占用的内存的垃圾收集器Garbage Collector

垃圾收集的例子

让我们创建看看它如何工作在下面的代码:

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)

这是内存结构图:

menu

在步骤(1),body.innerHTML被清空。所以,严格来说,它的孩子应该被删除,因为它们是不可达的。

但是元素#id是一个例外。它对于menu.elem是可达的,所以它仍然留着内存中。当然如果你检查它的父节点parentNode,其值会为null

个别的DOM元素可能依然留在内存即使父元素被清除。

在步骤(2),window.menu引用被重新分配,所以上一个menu变得不可访问。

它就会被浏览器垃圾收集器自动删除。

menu2

现在完整的menu结构被删除,包括元素。当然,如果有其他部分的代码引用了该元素,那么它会保持不变。

循环引用的收集

闭包往往会导致循环引用。例如:

function setHandler() {

  var elem = document.getElementById('id');

  elem.onclick = function() {
    // ...
  };

}

在这里,DOM元素直接通过onclick属性引用了函数。函数则通过外部的LexicalEnvironment对象引用了elem

ie1_1

即使处理函数内没有代码,也会出现此内存结构。特殊方法addEventListener / attachEvent同样会产生循环引用。

处理函数通常在elem元素删除的时候被清除:

function cleanUp() {
  var elem = document.getElementById('id');
  elem.parentNode.removeChild(elem);
}

调用cleanUp()函数将elem元素从DOM移除,elem仍然有一个引用LexicalEnvironment.elem,但是没有嵌套函数,所以LexicalEnvironment变量被回收了。之后,elem元素变成不可访问并随同它的处理函数一起被清除。

内存泄漏

当浏览器由于某些原因没有释放掉那些不再需要的对象时,便发生了内存泄漏。

这也许是由于浏览器bugs,浏览器拓展问题,或者是我们代码结构中的错误。

IE<8 DOM-JS 内存泄漏

Internet Explorer版本8之前是无法清除DOM对象和JavaScript之间的循环引用。

IE6在SP3(mid-2007 patch)之前,问题更严重,因为内存即使在页面关闭后也不释放。

因此,setHandler在IE<8下会有泄漏,elem和闭包从来不会被清理:

function setHandler() {
  var elem = document.getElementById('id');
  elem.onclick = function() { /* ... */ };
}

除了DOM元素,还有可能是XMLHttpRequest或任何其他COM对象。

IE泄漏的解决方法是打破循环引用。

ie2

我们指定elem = null,这样处理函数不再引用DOM元素,循环关系被打破。

这个泄漏的基本上是有一定历史了,但却是一个很好的打破循环关系的例子。

你可以阅读更多关于它的文章Understanding and Solving Internet Explorer Leak Patterns Circular Memory Leak Mitigation

XmlHttpRequest内存管理和泄漏

下面的代码在IE<9下泄漏:

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);

让我们看看每一步运行的内存结构:

xhr1

异步XMLHttpRequest对象是由浏览器追踪的。因此,有一个内部引用。

当请求完成后,引用被删除,所以xhr变得不可访问。但IE<9却办不到。

这有一个关于IE的单独页面的例子

幸运的是,修复这个问题是比较容易的。我们需要从闭包中移除xhr并在处理函数中以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;

xhr2

现在就没有循环引用,泄漏被修复。IE的例子页面

setInterval/setTimeout

setTimeout/setInterval中,函数也会被内部引用并随着其调用完成才被回收。

对于setInterval,则会在调用clearInterval时完成并回收。这可能会当函数实际上并没做什么,但定时器又没有被清除,而导致内存泄漏。

对于服务器端的JS和V8,可以看一个问题中的例子: 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函数留在内存时,含有一个大变量的LexicalEnvironment对象将会一直挂在内存中。

JavaScript解释器不知道哪些变量可能是内部函数需要的,所以它在每一个外部LexicalEnvironment对象进行完整保存。我希望,新的解释器可以试图优化它,但不确定能否成功。

事实上,有的可能并不是泄漏。许多函数可以被创建是有明确原因的,例如每一个请求,并没有被清除,因为他们是处理函数或其他。

如果变量data仅用于外部函数,我们可以让它节省内存。

function f() {
  var data = "Large piece of data, probably received from server";

  /* do something using data */

  function inner() {
    // ...
  }

  data = null;

  return inner;
}

现在data作为LexicalEnvironment对象的一个属性仍然保留在内存中,但它不会占用太多空间。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值