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

时间:2022-09-07 23:57:54

I've logged the following Chrome bug, which has led to many serious and non-obvious memory leaks in my code:

我记录了下面的Chrome bug,它导致我的代码中有很多严重而不明显的内存泄漏:

(These results use Chrome Dev Tools' memory profiler, which runs the GC, and then takes a heap snapshot of everything not garbaged collected.)

(这些结果使用了Chrome开发工具的内存分析器,该分析器运行GC,然后获取未被垃圾收集的所有内容的堆快照。)

In the code below, the someClass instance is garbage collected (good):

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

var someClass = function() {};

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

window.f_ = f();

But it won't be garbage collected in this case (bad):

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

var someClass = function() {};

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

window.f_ = f();

And the corresponding screenshot:

和相应的截图:

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

It seems that a closure (in this case, function() {}) keeps all objects "alive" if the object is referenced by any other closure in the same context, whether or not if that closure itself is even reachable.

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

My question is about garbage collection of closure in other browsers (IE 9+ and Firefox). I am quite familiar with webkit's tools, such as the JavaScript heap profiler, but I know little of other browsers' tools, so I haven't been able to test this.

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

In which of these three cases will IE9+ and Firefox garbage collect the someClass instance?

在这三种情况中,IE9+和Firefox垃圾会收集someClass实例?

3 个解决方案

#1


76  

As far as I can tell, this is not a bug but the expected behavior.

就我所知,这不是一个bug,而是预期的行为。

From Mozilla's Memory management page: "As of 2012, all modern browsers ship a mark-and-sweep garbage-collector." "Limitation: objects need to be made explicitly unreachable".

Mozilla的内存管理页面上写道:“到2012年,所有现代浏览器都推出了标记-清除垃圾收集器。”“限制:需要显式地使对象不可访问”。

In your examples where it fails some is still reachable in the closure. I tried two ways to make it unreachable and both work. Either you set some=null when you don't need it anymore, or you set window.f_ = null; and it will be gone.

在您的示例中,失败的部分仍然可以在闭包中找到。我尝试了两种方法使它不可到达,并且两者都有效。当不再需要它时,可以设置some=null,或者设置window。f =零;它就会消失。

Update

更新

I have tried it in Chrome 30, FF25, Opera 12 and IE10 on Windows.

我在Chrome 30、FF25、Opera 12和IE10 Windows上都试过。

The standard doesn't say anything about garbage collection, but gives some clues of what should happen.

该标准没有提到任何关于垃圾收集的内容,但是提供了一些应该发生什么的线索。

  • Section 13 Function definition, step 4: "Let closure be the result of creating a new Function object as specified in 13.2"
  • 第13节函数定义,第4步:“让闭包是创建一个新的函数对象的结果,它是在13.2中指定的。”
  • Section 13.2 "a Lexical Environment specified by Scope" (scope = closure)
  • 第13.2节“范围指定的词汇环境”(范围=闭包)
  • Section 10.2 Lexical Environments:
  • 10.2节词汇环境:

"The outer reference of a (inner) Lexical Environment is a reference to the Lexical Environment that logically surrounds the inner Lexical Environment.

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

An outer Lexical Environment may, of course, have its own outer Lexical Environment. A Lexical Environment may serve as the outer environment for multiple inner Lexical Environments. For example, if a Function Declaration contains two nested Function Declarations then the Lexical Environments of each of the nested functions will have as their outer Lexical Environment the Lexical Environment of the current execution of the surrounding function."

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

So, a function will have access to the environment of the parent.

因此,一个函数可以访问父函数的环境。

So, some should be available in the closure of the returning function.

因此,在返回函数的闭包中应该有一些可用的。

Then why isn't it always available?

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

It seems that Chrome and FF is smart enough to eliminate the variable in some cases, but in both Opera and IE the some variable is available in the closure (NB: to view this set a breakpoint on return null and check the debugger).

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

The GC could be improved to detect if some is used or not in the functions, but it will be complicated.

可以对GC进行改进,以检测函数中是否使用了某些GC,但这将非常复杂。

A bad example:

一个坏的例子:

var someClass = function() {};

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

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

In example above the GC has no way of knowing if the variable is used or not (code tested and works in Chrome30, FF25, Opera 12 and IE10).

在上面的例子中,GC没有办法知道变量是否被使用(代码在Chrome30、FF25、Opera 12和IE10中测试和工作)。

The memory is released if the reference to the object is broken by assigning another value to window.f_.

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

In my opinion this isn't a bug.

在我看来,这不是一个bug。

#2


47  

I tested this in IE9+ and Firefox.

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

Live site here.

网站在这里生活。

I hoped to wind up with an array of 500 function() {}'s, using minimal memory.

我希望最后得到一个包含500个函数()的数组,使用最小的内存。

Unfortunately, that was not the case. Each empty function holds on to an (forever unreachable, but not GC'ed) array of a million numbers.

不幸的是,事实并非如此。每个空函数都保存在一个(永远无法访问的,但不是GC'ed)数组中。

Chrome eventually halts and dies, Firefox finishes the whole thing after using nearly 4GB of RAM, and IE grows asymptotically slower until it shows "Out of memory".

Chrome最终会停止和消亡,Firefox在使用了近4GB的RAM之后完成了这一切,而IE则会逐渐变慢,直到“内存耗尽”。

Removing either one of the commented lines fixes everything.

删除其中一个注释行可以修复所有问题。

It seems that all three of these browsers (Chrome, Firefox, and IE) keep an environment record per context, not per closure. Boris hypothesizes the reason behind this decision is performance, and that seems likely, though I'm not sure how performant it can be called in light of the above experiment.

这三种浏览器(Chrome、Firefox和IE)似乎都在每个上下文(而不是每个闭包)保持一个环境记录。鲍里斯假设这个决定背后的原因是性能,这似乎是可能的,尽管我不确定根据上面的实验,它能被称为什么样的性能。

If a need a closure referencing some (granted I didn't use it here, but imagine I did), if instead of

如果需要引用某个闭包(假设这里我没有使用它,但假设我使用了它),如果不是的话

function g() { some; }

I use

我使用

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

it will fix the memory problems by moving the closure to a different context than my other function.

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

This will make my life much more tedious.

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

P.S. Out of curiousity, I tried this in Java (using its ability to define classes inside of functions). GC works as I had originally hoped for Javascript.

出于好奇,我在Java中尝试了这个方法(使用它在函数内部定义类的能力)。GC的工作方式符合我最初对Javascript的期望。

#3


14  

Heuristics vary, but a common way to implement this sort of thing is to create an environment record for each call to f() in your case, and only store the locals of f that are actually closed over (by some closure) in that environment record. Then any closure created in the call to f keeps alive the environment record. I believe this is how Firefox implements closures, at least.

Heuristics不同,但是实现这类事情的一种常见方法是为每个调用f()创建一个环境记录,并且只存储在该环境记录中实际关闭的f(通过一些闭包)的本地函数。然后,在调用f中创建的任何闭包都会使环境记录保持活动状态。我相信这就是Firefox实现闭包的方式。

This has the benefits of fast access to closed-over variables and simplicity of implementation. It has the drawback of the observed effect, where a short-lived closure closing over some variable causes it to be kept alive by long-lived closures.

这具有快速访问封闭变量和实现简单性的优点。它有观察到的效果的缺点,即对某些变量的短期闭包关闭会使它通过长期闭包保持活性。

One could try creating multiple environment records for different closures, depending on what they actually close over, but that can get very complicated very quickly and can cause performance and memory problems of its own...

可以尝试为不同的闭包创建多个环境记录,这取决于它们实际关闭的是什么,但是这会很快变得非常复杂,并可能导致自身的性能和内存问题……

#1


76  

As far as I can tell, this is not a bug but the expected behavior.

就我所知,这不是一个bug,而是预期的行为。

From Mozilla's Memory management page: "As of 2012, all modern browsers ship a mark-and-sweep garbage-collector." "Limitation: objects need to be made explicitly unreachable".

Mozilla的内存管理页面上写道:“到2012年,所有现代浏览器都推出了标记-清除垃圾收集器。”“限制:需要显式地使对象不可访问”。

In your examples where it fails some is still reachable in the closure. I tried two ways to make it unreachable and both work. Either you set some=null when you don't need it anymore, or you set window.f_ = null; and it will be gone.

在您的示例中,失败的部分仍然可以在闭包中找到。我尝试了两种方法使它不可到达,并且两者都有效。当不再需要它时,可以设置some=null,或者设置window。f =零;它就会消失。

Update

更新

I have tried it in Chrome 30, FF25, Opera 12 and IE10 on Windows.

我在Chrome 30、FF25、Opera 12和IE10 Windows上都试过。

The standard doesn't say anything about garbage collection, but gives some clues of what should happen.

该标准没有提到任何关于垃圾收集的内容,但是提供了一些应该发生什么的线索。

  • Section 13 Function definition, step 4: "Let closure be the result of creating a new Function object as specified in 13.2"
  • 第13节函数定义,第4步:“让闭包是创建一个新的函数对象的结果,它是在13.2中指定的。”
  • Section 13.2 "a Lexical Environment specified by Scope" (scope = closure)
  • 第13.2节“范围指定的词汇环境”(范围=闭包)
  • Section 10.2 Lexical Environments:
  • 10.2节词汇环境:

"The outer reference of a (inner) Lexical Environment is a reference to the Lexical Environment that logically surrounds the inner Lexical Environment.

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

An outer Lexical Environment may, of course, have its own outer Lexical Environment. A Lexical Environment may serve as the outer environment for multiple inner Lexical Environments. For example, if a Function Declaration contains two nested Function Declarations then the Lexical Environments of each of the nested functions will have as their outer Lexical Environment the Lexical Environment of the current execution of the surrounding function."

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

So, a function will have access to the environment of the parent.

因此,一个函数可以访问父函数的环境。

So, some should be available in the closure of the returning function.

因此,在返回函数的闭包中应该有一些可用的。

Then why isn't it always available?

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

It seems that Chrome and FF is smart enough to eliminate the variable in some cases, but in both Opera and IE the some variable is available in the closure (NB: to view this set a breakpoint on return null and check the debugger).

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

The GC could be improved to detect if some is used or not in the functions, but it will be complicated.

可以对GC进行改进,以检测函数中是否使用了某些GC,但这将非常复杂。

A bad example:

一个坏的例子:

var someClass = function() {};

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

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

In example above the GC has no way of knowing if the variable is used or not (code tested and works in Chrome30, FF25, Opera 12 and IE10).

在上面的例子中,GC没有办法知道变量是否被使用(代码在Chrome30、FF25、Opera 12和IE10中测试和工作)。

The memory is released if the reference to the object is broken by assigning another value to window.f_.

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

In my opinion this isn't a bug.

在我看来,这不是一个bug。

#2


47  

I tested this in IE9+ and Firefox.

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

Live site here.

网站在这里生活。

I hoped to wind up with an array of 500 function() {}'s, using minimal memory.

我希望最后得到一个包含500个函数()的数组,使用最小的内存。

Unfortunately, that was not the case. Each empty function holds on to an (forever unreachable, but not GC'ed) array of a million numbers.

不幸的是,事实并非如此。每个空函数都保存在一个(永远无法访问的,但不是GC'ed)数组中。

Chrome eventually halts and dies, Firefox finishes the whole thing after using nearly 4GB of RAM, and IE grows asymptotically slower until it shows "Out of memory".

Chrome最终会停止和消亡,Firefox在使用了近4GB的RAM之后完成了这一切,而IE则会逐渐变慢,直到“内存耗尽”。

Removing either one of the commented lines fixes everything.

删除其中一个注释行可以修复所有问题。

It seems that all three of these browsers (Chrome, Firefox, and IE) keep an environment record per context, not per closure. Boris hypothesizes the reason behind this decision is performance, and that seems likely, though I'm not sure how performant it can be called in light of the above experiment.

这三种浏览器(Chrome、Firefox和IE)似乎都在每个上下文(而不是每个闭包)保持一个环境记录。鲍里斯假设这个决定背后的原因是性能,这似乎是可能的,尽管我不确定根据上面的实验,它能被称为什么样的性能。

If a need a closure referencing some (granted I didn't use it here, but imagine I did), if instead of

如果需要引用某个闭包(假设这里我没有使用它,但假设我使用了它),如果不是的话

function g() { some; }

I use

我使用

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

it will fix the memory problems by moving the closure to a different context than my other function.

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

This will make my life much more tedious.

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

P.S. Out of curiousity, I tried this in Java (using its ability to define classes inside of functions). GC works as I had originally hoped for Javascript.

出于好奇,我在Java中尝试了这个方法(使用它在函数内部定义类的能力)。GC的工作方式符合我最初对Javascript的期望。

#3


14  

Heuristics vary, but a common way to implement this sort of thing is to create an environment record for each call to f() in your case, and only store the locals of f that are actually closed over (by some closure) in that environment record. Then any closure created in the call to f keeps alive the environment record. I believe this is how Firefox implements closures, at least.

Heuristics不同,但是实现这类事情的一种常见方法是为每个调用f()创建一个环境记录,并且只存储在该环境记录中实际关闭的f(通过一些闭包)的本地函数。然后,在调用f中创建的任何闭包都会使环境记录保持活动状态。我相信这就是Firefox实现闭包的方式。

This has the benefits of fast access to closed-over variables and simplicity of implementation. It has the drawback of the observed effect, where a short-lived closure closing over some variable causes it to be kept alive by long-lived closures.

这具有快速访问封闭变量和实现简单性的优点。它有观察到的效果的缺点,即对某些变量的短期闭包关闭会使它通过长期闭包保持活性。

One could try creating multiple environment records for different closures, depending on what they actually close over, but that can get very complicated very quickly and can cause performance and memory problems of its own...

可以尝试为不同的闭包创建多个环境记录,这取决于它们实际关闭的是什么,但是这会很快变得非常复杂,并可能导致自身的性能和内存问题……