JavaScript闭包如何被垃圾回收


168

我已记录以下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();

以及相应的截图:

Chromebug的屏幕截图

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

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

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


4
对于初学者来说,Chrome如何让您测试哪些变量/对象被垃圾回收了,以及何时发生?
nnnnnn 2013年

1
也许控制台正在引用它。清除控制台时,它是否被GC?
大卫

1
@david在最后一个示例中,该unreachable函数从未执行过,因此实际上未记录任何内容。
James Montagne

1
即使我们似乎面对事实,我也很难相信会发生如此重要的错误。但是,我一次又一次地查看代码,没有找到其他任何合理的解释。您试图完全不在控制台中运行代码(也就是让浏览器从加载的脚本中自然运行它)吗?
plalx

1
@some,我之前看过那篇文章。它的副标题为“在JavaScript应用程序中处理循环引用”,但是对JS / DOM循环引用的关注不适用于任何现代浏览器。它提到了闭包,但是在所有示例中,有问题的变量仍在程序中使用。
Paul Draper 2013年

Answers:


78

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

来自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使用了GC ,但这将很复杂。

一个不好的例子:

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_

我认为这不是错误。


4
但是,一旦setTimeout()回调运行,回调函数的作用域setTimeout()就完成了,整个作用域应被垃圾回收,释放对的引用some。不再有任何可以运行的代码可以到达some闭包中的实例。应该将其收集起来。最后一个例子更糟,因为unreachable()甚至没有调用,也没有人引用它。它的范围也应该被GC化。这些看起来都像是虫子。JS中没有语言要求来“释放”函数范围内的内容。
jfriend00 2013年

1
@some不应该。不应该在内部不使用函数的情况下关闭函数。
plalx

2
可以通过empty函数访问它,但是不是,因此没有实际引用,因此应该清楚。垃圾收集会跟踪实际引用。它不应保留可能已经引用的所有内容,而仅保留实际引用的内容。一旦f()调用了最后一个,就不再有实际引用some。它无法访问,应该被GC。
jfriend00 2013年

1
@ jfriend00我在(标准)[ ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf]中找不到任何内容,只说了它在内部使用的变量。在第13节中,生成步骤4:让闭包成为创建13.2和10.2中指定的新Function对象的结果 “外部环境引用用于对词法环境值的逻辑嵌套进行建模。(inner )词法环境是对在逻辑上围绕内部词法环境的词法环境的引用。”
一些

2
好吧,eval真的是特例。例如,eval不能使用别名(developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…),例如var eval2 = eval。如果eval使用了(并且由于不能用其他名称调用它,这很容易做到),那么我们必须假定它可以在范围内使用任何东西。
保罗·德雷珀

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

现场直播在这里

我希望以function() {}最少的内存完成500个数组。

不幸的是,事实并非如此。每个空函数都保留着一百万个数字的数组(永远不可达,但不是GC'ed)。

Chrome最终停止运行并死了,Firefox使用了将近4GB的内存后完成了整个工作,IE渐渐地变慢,直到显示“ Out of memory”。

删除任一注释行即可修复所有问题。

似乎所有这三种浏览器(Chrome,Firefox和IE)都按上下文而不是按关闭记录环境记录。鲍里斯(Boris)提出了做出此决定的原因是性能,这似乎是可能的,尽管我不确定根据上述实验该如何称呼性能。

如果需要闭包引用some(当然,我没有在这里使用它,但是想像我在使用它),如果不是

function g() { some; }

我用

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

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

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

PS出于好奇,我在Java中尝试了此操作(利用其在函数内部定义类的能力)。GC可以像我最初希望的那样运行。


我认为外部函数var g =(function(some){return function(){some;};})(some);
HCJ 2014年

15

启发式方法各不相同,但是实现此类操作的一种常用方法是为f()您的案例中的每个调用创建一个环境记录,并仅将f实际关闭(通过某种关闭)关闭的本地存储在该环境记录中。然后,在调用中创建的所有闭包f使环境记录保持活动状态。我相信至少这是Firefox实现关闭的方式。

这样的好处是可以快速访问封闭变量,并且实现简单。它具有观察到的效果的缺点,即在某些变量上关闭的短期关闭会导致长期关闭而使它保持活动状态。

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


感谢您的见解。我已经得出结论,这也是Chrome同样实现闭包的方式。我一直以为它们是用后一种方式实现的,即每个闭包仅保留所需的环境,但事实并非如此。我想知道创建多个环境记录是否真的那么复杂。与其汇总闭包的引用,不如将每个闭包作为唯一的闭包。我曾经猜想性能是这里的原因,尽管对我而言,共享环境记录的后果似乎更糟。
Paul Draper 2013年

在某些情况下,后一种方法导致需要创建的环境记录数量激增。除非您尽力在功能之间共享它们,否则您将需要一堆复杂的设备来做到这一点。这是可能的,但是我被告知性能折衷支持当前的方法。
Boris Zbarsky

记录数等于创建的关闭数。我可能描述O(n^2)O(2^n)作为爆炸,但没有成比例增加。
Paul Draper 2013年

好吧,与O(1)相比,O(N)是爆炸式增长,尤其是当每个人都可以占用大量内存时……再次,我不是专家。在irc.mozilla.org上的#jsapi频道上提问可能比我能提供的折衷方法给您更好,更详细的解释。
Boris Zbarsky

1
@Esailija不幸的是,这实际上很普遍。您所需要的只是函数中的一个大临时变量(通常是一个大类型数组),该临时变量可以使用一些随机的短期回调和一个长期的闭包。最近有人编写Web应用程序出现了很多次……
Boris Zbarsky 2013年

0
  1. 保持函数调用之间的状态假设您有函数add(),并且希望它在几次调用中将传递给它的所有值相加并返回总和。

像add(5); //返回5

加(20); //返回25(5 + 20)

add(3); //返回28(25 + 3)

您可以通过两种方式首先执行此操作:正常定义全局变量 当然,您可以使用全局变量来保存总数。但是请记住,如果您(不使用)全局变量,那么这个家伙会吞噬您的生命。

现在使用闭包而不定义全局变量的最新方法

(function(){

  var addFn = function addFn(){

    var total = 0;
    return function(val){
      total += val;
      return total;
    }

  };

  var add = addFn();

  console.log(add(5));
  console.log(add(20));
  console.log(add(3));
  
}());


0

function Country(){
    console.log("makesure country call");	
   return function State(){
   
    var totalstate = 0;	
	
	if(totalstate==0){	
	
	console.log("makesure statecall");	
	return function(val){
      totalstate += val;	 
      console.log("hello:"+totalstate);
	   return totalstate;
    }	
	}else{
	 console.log("hey:"+totalstate);
	}
	 
  };  
};

var CA=Country();
 
 var ST=CA();
 ST(5); //we have add 5 state
 ST(6); //after few year we requare  have add new 6 state so total now 11
 ST(4);  // 15
 
 var CB=Country();
 var STB=CB();
 STB(5); //5
 STB(8); //13
 STB(3);  //16

 var CX=Country;
 var d=Country();
 console.log(CX);  //store as copy of country in CA
 console.log(d);  //store as return in country function in d


请描述答案
janith1024 '18

0

(function(){

   function addFn(){

    var total = 0;
	
	if(total==0){	
	return function(val){
      total += val;	 
      console.log("hello:"+total);
	   return total+9;
    }	
	}else{
	 console.log("hey:"+total);
	}
	 
  };

   var add = addFn();
   console.log(add);  
   

    var r= add(5);  //5
	console.log("r:"+r); //14 
	var r= add(20);  //25
	console.log("r:"+r); //34
	var r= add(10);  //35
	console.log("r:"+r);  //44
	
	
var addB = addFn();
	 var r= addB(6);  //6
	 var r= addB(4);  //10
	  var r= addB(19);  //29
    
  
}());

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.