JavaScript 闭包是如何被垃圾回收的

我记录了以下Chrome错误,这导致我的代码中出现了许多严重且不明显的内存泄漏:

(这些结果使用Chrome Dev Tools的内存探查器,该分析器运行GC,然后拍摄未垃圾回收的所有内容的堆快照。

在下面的代码中,实例是垃圾回收(好):someClass

var someClass = function() {};

function f() {
  var some = new someClass();
  return function() {};
}

window.f_ = f();

但在这种情况下,它不会被垃圾收集(不好):

var someClass = function() {};

function f() {
  var some = new someClass();
  function unreachable() { some; }
  return function() {};
}

window.f_ = f();

以及相应的屏幕截图:

screenshot of Chromebug

如果该对象在同一上下文中被任何其他闭包引用,则闭包(在本例中 )似乎使所有对象保持“活动”状态,无论该闭包本身是否可访问。function() {}

我的问题是关于其他浏览器(IE 9 +和Firefox)中关闭的垃圾回收。我非常熟悉webkit的工具,例如JavaScript堆分析器,但我对其他浏览器的工具知之甚少,所以我无法对此进行测试。

在这三种情况下,IE9+和Firefox垃圾回收实例的哪一种someClass


答案 1

据我所知,这不是一个错误,而是预期的行为。

来自Mozilla的内存管理页面:“截至2012年,所有现代浏览器都附带了一个标记和扫描垃圾收集器。“限制:需要使对象明确无法访问”。

在您的示例中,它失败仍然可以通过闭包进行访问。我尝试了两种方法使其无法访问,并且都有效。要么你在不再需要它的时候设置,要么你设置它,它就会消失。somesome=nullwindow.f_ = null;

更新

我已经在Windows上的Chrome 30,FF25,Opera 12和IE10中尝试过。

标准没有说明垃圾回收的任何信息,但提供了一些关于应该发生的事情的线索。

  • 第 13 节 函数定义,步骤 4:“让闭包成为创建 13.2 中指定的新 Function 对象的结果”
  • 第 13.2 节 “作用域指定的词汇环境”(作用域 = 闭包)
  • 第 10.2 节 词法环境:

“(内部)词汇环境的外部引用是对逻辑上围绕内部词汇环境的词汇环境的引用。

当然,外部词汇环境可能有自己的外部词汇环境。词法环境可以用作多个内部词法环境的外部环境。例如,如果函数声明包含两个嵌套的函数声明,则每个嵌套函数的词法环境将具有当前执行周围函数的词法环境作为其外部词法环境。

因此,函数将有权访问父函数的环境。

所以,应该在返回函数的闭包中可用。some

那么为什么它并不总是可用呢?

在某些情况下,Chrome 和 FF 似乎足够聪明,可以消除变量,但在 Opera 和 IE 中,变量在闭包中都可用(注意:查看此设置断点并检查调试器)。somereturn null

可以改进GC以检测是否在功能中使用,但会很复杂。some

一个不好的例子:

var someClass = function() {};

function f() {
  var some = new someClass();
  return function(code) {
    console.log(eval(code));
  };
}

window.f_ = f();
window.f_('some');

在上面的例子中,GC无法知道变量是否被使用(代码经过测试,可在Chrome30,FF25,Opera 12和IE10中使用)。

如果通过将另一个值赋给 而破坏了对该对象的引用,则会释放内存。window.f_

在我看来,这不是一个错误。


答案 2

我在IE9 +和Firefox中测试了这一点。

function f() {
  var some = [];
  while(some.length < 1e6) {
    some.push(some.length);
  }
  function g() { some; } //removing this fixes a massive memory leak
  return function() {};   //or removing this
}

var a = [];
var interval = setInterval(function() {
  var len = a.push(f());
  if(len >= 500) {
    clearInterval(interval);
  }
}, 10);

现场网站在这里

我希望最终使用最少的内存来排列500个阵列。function() {}

不幸的是,事实并非如此。每个空函数都保留一个包含一百万个数字的(永远无法访问,但不是GC'ed)数组。

Chrome最终停止并死亡,Firefox在使用近4GB的RAM后完成了整个过程,IE逐渐变慢,直到它显示“内存不足”。

删除其中任何一个注释行都可以修复所有内容。

似乎所有这三个浏览器(Chrome,Firefox和IE)都为每个上下文而不是每个关闭都保留了环境记录。鲍里斯假设这个决定背后的原因是性能,这似乎是可能的,尽管我不确定根据上述实验可以调用它的性能如何。

如果需要一个闭包引用(当然我没有在这里使用它,但想象一下我做了),如果而不是some

function g() { some; }

我使用

var g = (function(some) { return function() { some; }; )(some);

它将通过将闭包移动到与我的其他函数不同的上下文来解决内存问题。

这将使我的生活更加乏味。

附言:出于好奇,我在Java中尝试了这个(利用它的能力在函数中定义类)。GC的工作原理与我最初对Javascript的期望一样。