JavaScript 闭包如何被垃圾回收

170
我已经记录了以下的Chrome bug,这导致我的代码中出现了许多严重且不明显的内存泄漏问题:
(这些结果使用Chrome Dev Tools的memory profiler,它运行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实例进行垃圾回收:


4
对于未接触过的人,Chrome如何让你测试哪些变量/对象被垃圾回收以及何时发生呢? - nnnnnn
1
也许控制台正在保留对它的引用。当您清除控制台时,它是否被垃圾回收? - david
1
@david 在最后一个例子中,unreachable 函数从未被执行,因此实际上没有任何内容被记录。 - James Montagne
1
我很难相信那么重要的一个bug竟然能够通过,即使我们似乎面对着事实。然而,我一遍又一遍地检查代码,但是找不到其他合理的解释。你尝试过不在控制台中运行代码吗(即让浏览器自然地从加载的脚本中运行)? - plalx
1
@some,我之前读过那篇文章。它的副标题是“在JavaScript应用程序中处理循环引用”,但JS / DOM循环引用的问题并不适用于任何现代浏览器。它提到了闭包,但在所有示例中,有关变量仍然可能被程序使用。 - Paul Draper
显示剩余10条评论
6个回答

78
据我所知,这不是一个 bug,而是预期的行为。根据 Mozilla 的 Memory management page:“截至 2012 年,所有现代浏览器都提供标记清除垃圾回收器。”“限制:对象需要显式地变得不可达
在您的例子中,当闭包中仍然可以访问 some 时,它会失败。我尝试了两种方法使其不可达,两种方式都有效。要么在不再需要它时将 some=null,要么将 window.f_ = null;,那么它就消失了。 更新 我已经在 Windows 上的 Chrome 30、FF25、Opera 12 和 IE10 中尝试过了。 standard 没有关于垃圾回收的说明,但给出了一些应该发生的线索。
  • 第13节函数定义,第4步:“让闭包成为根据13.2规定创建的新Function对象的结果”
  • 第13.2节“由Scope指定的词法环境”(scope = 闭包)
  • 第10.2节词法环境:

“(内部)词法环境的外部引用是指逻辑上包围内部词法环境的词法环境的引用。

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

因此,函数将可以访问父级的环境。

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

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

看起来Chrome和FF在某些情况下足够聪明,可以消除变量,在Opera和IE中,闭包中仍然存在some变量(注:要查看这个,请在return null上设置断点并检查调试器)。

垃圾收集器可以改进以检测函数中是否使用了some,但这将很复杂。

一个糟糕的例子:

var someClass = function() {};

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

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

在上面的示例中,垃圾回收器无法知道变量是否被使用(在Chrome30、FF25、Opera 12和IE10中测试过代码并且可行)。
如果通过将另一个值分配给window.f_来中断对对象的引用,则释放内存。
在我看来,这不是一个错误。

4
然而,一旦setTimeout()回调函数运行,该函数作用域的生命周期结束,整个作用域应该被垃圾回收,释放对some的引用。在闭包中没有任何代码可以访问some的实例了。它应该被垃圾回收。最后一个示例甚至更糟,因为unreachable()甚至没有被调用,也没有人引用它。它的作用域也应该被垃圾回收。这两种情况都似乎是错误。在JS中没有语言要求“释放”函数作用域中的内容。 - jfriend00
2
它可以通过空函数访问,但实际上没有引用它,因此应该很清楚。垃圾回收跟踪实际引用。它不应该保留可能被引用的所有内容,只保留实际引用的内容。一旦最后一个 f() 被调用,就不再有对 some 的实际引用了。它是不可访问的,应该被垃圾回收。 - jfriend00
1
@jfriend00 我在(标准文档)[http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf] 中没有找到任何关于只有内部使用的变量应该可用的内容。在第13节中,生产步骤4:让闭包成为按照13.2规定创建的新函数对象的结果,10.2“外部环境引用用于模拟词法环境值的逻辑嵌套。 (内部) 词法环境的外部引用是指逻辑上包围内部词法环境的词法环境的引用。” - some
2
嗯,eval是一个非常特殊的情况。例如,eval不能被别名化(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval),例如 var eval2 = eval。如果使用了eval(并且由于它不能用不同的名称调用,所以很容易这样做),那么我们必须假设它可以使用范围内的任何内容。 - Paul Draper
1
因为这个答案没有抓住重点,所以被踩了。基本上,规范说明程序应该做什么,而不是不应该做什么;也没有规定特定的实现方式。函数需要能够访问其词法作用域,但这并不意味着必须有一个“词法作用域”对象(虽然可能会有),也不意味着该作用域需要实际持久存储任何东西——它只需要能够访问它;但如果它不需要,为什么要存储任何东西呢?此外,它并没有说无法访问的内容必须被释放。更好的解释:https://dev59.com/KGIj5IYBdhLWcg3w4Izt#19803948 - Eamon Nerbonne
显示剩余20条评论

50

我在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处理)一百万数字的数组。

Chrome最终会停止运行并崩溃,Firefox在使用将近4GB的RAM后完成整个任务,而IE则会增长得越来越慢,直到显示“内存不足”。

去掉其中任何一行注释都可以解决所有问题。

似乎这三个浏览器(Chrome、Firefox和IE)每个上下文都保留一个环境记录,而不是每个闭包保留一个。Boris提出的假设是出于性能考虑做出了这个决定,虽然考虑到以上实验结果,这种决策可能并非高效。

如果我需要一个引用some的闭包(尽管我在这里没有使用它,但请想象我使用了它),如果不是:

function g() { some; }
我使用
var g = (function(some) { return function() { some; }; )(some);

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

这将使我的生活更加繁琐。

P.S. 出于好奇,我在Java中尝试了这个方法(使用其在函数内定义类的能力)。 GC 的工作方式与我最初在 JavaScript 中希望的相同。


我认为外部函数的括号未关闭。var g =(function(some){return function(){some;};} )(some); - HCJ
我想知道最新的JS引擎是否仍然是这种情况? - jfriend00

16
启发式方法有所不同,但实现这种方式的常见方法是为每次调用f()创建一个环境记录,并仅在该环境记录中存储实际上被某个闭包封闭的f 的局部变量。然后,调用f()时创建的任何闭包都会保持环境记录的活动状态。我相信这至少是Firefox实现闭包的方式。
这样做有快速访问封闭变量和简单实现的好处。它存在观察到的效果的缺点,即闭包封闭一些变量的短暂生命周期会被长期生命周期的闭包所保留,导致这些变量无法被及时释放。
可以尝试为不同的闭包创建多个环境记录,具体取决于它们实际上封闭了什么,但这可能很快变得非常复杂,并且可能会引起性能和内存问题……

1
谢谢您的见解。我得出结论,这也是Chrome实现闭包的方式。我一直以为它们是以后一种方式实现的,即每个闭包只保留它所需的环境,但事实并非如此。我想知道创建多个环境记录是否真的那么复杂。与其聚合闭包的引用,不如将每个引用视为唯一的闭包。我猜测这里的原因是性能考虑,尽管对我来说,共享环境记录的后果甚至更糟。 - Paul Draper
后一种方式在某些情况下会导致需要创建大量环境记录的爆炸性增长。除非您尽力在函数之间共享它们,但这需要大量复杂的机制来实现。虽然这是可能的,但我被告知性能权衡有利于当前的方法。 - Boris Zbarsky
值得注意的是,由于触发这个问题需要使用一些非常不可思议的代码进行故意设置,因此实际上并不存在实际问题。 - Esailija
@Esailija 触发了什么? - Boris Zbarsky
2
@Esailija,这种情况实际上很常见,不幸的是。你只需要在函数中使用一个大的临时变量(通常是一个大的类型数组),让一些随机的短暂回调函数使用,并使用一个长期存在的闭包。最近,对于编写Web应用程序的人们来说,这种情况已经出现了多次... - Boris Zbarsky
显示剩余3条评论

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

例如: add(5); // 返回 5

add(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));
  
}());


-1

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

-1

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接