JavaScript封闭是如何被垃圾收集的

Pau*_*per 166 javascript firefox internet-explorer garbage-collection google-chrome

我已经记录了以下Chrome错误,这导致我的代码中存在许多严重且非显而易见的内存泄漏:

(这些结果使用Chrome Dev Tools的内存分析器,它运行GC,然后获取未收集的所有内容的堆快照.)

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

var someClass = function() {};

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

window.f_ = f();
Run Code Online (Sandbox Code Playgroud)

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

var someClass = function() {};

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

window.f_ = f();
Run Code Online (Sandbox Code Playgroud)

和相应的截图:

Chromebug的屏幕截图

似乎闭包(在这种情况下,function() {})如果对象被同一上下文中的任何其他闭包引用,则所有对象都保持"活动",无论该闭包本身是否可以访问.

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

在这三种情况中哪些将IE9 +和Firefox垃圾收集 someClass 实例?

som*_*ome 77

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

来自Mozilla的内存管理页面:"截至2012年,所有现代浏览器都提供了一个标记清除垃圾收集器." "限制:需要明确无法访问对象 ".

在您的示例中,它some仍然可以在闭包中找到它.我尝试了两种方法使它无法访问并且都可以工作.要么你some=null不再需要它时设置,要么你设置window.f_ = null;它就会消失.

更新

我在Windows上的Chrome 30,FF25,Opera 12和IE10中尝试过它.

标准没有说明垃圾收集的任何内容,但提供了一些应该发生的线索.

  • 第13节函数定义,第4步:"让闭包成为创建13.2中指定的新Function对象的结果"
  • 第13.2节"范围指定的词汇环境"(范围=关闭)
  • 第10.2节词汇环境:

"(内部)词汇环境的外部参考是对词汇环境的引用,它在逻辑上围绕着内部的词汇环境.

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

因此,函数将可以访问父项的环境.

因此,some应该在返回函数的闭包中可用.

那为什么不总是可用?

在某些情况下,Chrome和FF似乎足够智能消除变量,但在Opera和IE中,some变量在闭包中可用(注意:查看此设置断点return 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');
Run Code Online (Sandbox Code Playgroud)

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

如果通过分配另一个值来破坏对象的引用,则释放内存window.f_.

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

  • 但是,一旦`setTimeout()`回调运行,`setTimeout()`回调的函数范围就完成了,整个范围应该被垃圾收集,释放它对`some`的引用.不再有任何可以运行的代码可以到达闭包中的`some`实例.它应该是垃圾收集.最后一个例子甚至更糟,因为`unreachable()`甚至没有被调用,没有人对它有引用.它的范围也应该是GCed.这些都看起来像错误.JS中没有语言要求在功能范围内"释放"东西. (4认同)
  • 可以通过empty函数访问它,但是不是,因此没有实际引用,因此应该清楚。垃圾收集会跟踪实际引用。它不应保留可能已经引用的所有内容,而仅保留实际引用的内容。一旦最后一个`f()`被调用,就不再有对`some`的实际引用。它无法访问,应该被GC。 (2认同)
  • 那么,'eval`真的很特殊.例如,`eval`不能别名(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval),例如`var eval2 = eval`.如果使用`eval`(因为它不能用不同的名称调用,这很容易),那么我们必须假设它可以使用范围内的任何东西. (2认同)

Pau*_*per 49

我在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);
Run Code Online (Sandbox Code Playgroud)

现场直播在这里.

我希望function() {}用最少的内存来完成500的阵列.

不幸的是,事实并非如此.每个空函数都持有一个(永远无法访问但不是GC)数百万个数组.

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

删除其中一条注释行可修复所有内容.

似乎这三种浏览器(Chrome,Firefox和IE)都保留了每个上下文的环境记录,而不是每个闭包.鲍里斯假设这个决定背后的原因是表现,这似乎很可能,虽然我不确定如何根据上述实验调用它.

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

function g() { some; }
Run Code Online (Sandbox Code Playgroud)

我用

var g = (function(some) { return function() { some; }; )(some);
Run Code Online (Sandbox Code Playgroud)

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

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

PS出于好奇,我在Java中尝试了这一点(使用它在函数内部定义类的能力).GC的工作方式与我原先希望的Javascript一样.


Bor*_*sky 15

启发式方法各不相同,但实现此类事情的常用方法是为f()您的案例中的每个调用创建一个环境记录,并且仅存储该环境记录f中实际已关闭(通过某些闭包)的本地文件.然后在调用中创建的任何闭包f保持环境记录.我相信这是Firefox实现闭包的方式,至少.

这具有快速访问封闭变量和实现简单的优点.它具有观察到的效果的缺点,其中关闭某个变量的短暂闭合使得它通过长寿命闭合保持活着.

可以尝试为不同的闭包创建多个环境记录,具体取决于它们实际关闭的内容,但是这可能非常快速地变得非常复杂并且可能导致其自身的性能和内存问题...

  • @Esailija 不幸的是,这实际上很常见。您所需要的只是函数中的一个大型临时文件(通常是一个大型类型数组),一些随机的短期回调使用它和一个长期的闭包。最近,对于编写网络应用程序的人来说,这个问题出现了很多次...... (2认同)