【问题标题】:How JavaScript closures are garbage collectedJavaScript 闭包是如何被垃圾回收的
【发布时间】:2013-11-16 21:45:03
【问题描述】:

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

(这些结果使用 Chrome 开发工具的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();

以及对应的截图:

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

我的问题是关于其他浏览器(IE 9+ 和 Firefox)中闭包的垃圾收集。我对 webkit 的工具很熟悉,比如 JavaScript heap profiler,但是我对其他浏览器的工具知之甚少,所以我一直无法测试。

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

【问题讨论】:

  • 对于初学者来说,Chrome 如何让您测试哪些变量/对象被垃圾回收,以及何时发生?
  • 也许控制台会保留对它的引用。当你清除控制台时它会被 GCed 吗?
  • @david 在最后一个示例中,unreachable 函数从未执行,因此实际上没有记录任何内容。
  • 我很难相信这么重要的错误已经发生了,即使我们似乎面对事实。但是我一次又一次地查看代码,我没有找到任何其他合理的解释。你试图完全不在控制台中运行代码(也就是让浏览器从加载的脚本中自然地运行它)?
  • @some,我以前读过那篇文章。它的副标题是“处理 JavaScript 应用程序中的循环引用”,但 JS/DOM 循环引用的问题不适用于现代浏览器。它提到了闭包,但在所有示例中,有问题的变量仍然可能被程序使用。

标签: javascript internet-explorer google-chrome firefox garbage-collection


【解决方案1】:

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

来自 Mozilla 的 Memory management page:“截至 2012 年,所有现代浏览器都提供了标记和清除垃圾收集器。” “限制:对象需要明确地不可访问

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

更新

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

standard 没有说明垃圾收集,但提供了一些关于应该发生什么的线索。

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

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

一个外部的词法环境当然可以有它自己的外部 词汇环境。一个词汇环境可以作为多个内部词汇的外部环境 环境。例如,如果一个 Function Declaration 包含两个嵌套的 Function Declarations,那么 Lexical 每个嵌套函数的环境将具有作为其外部词法环境的词法 当前执行周边函数的环境。"

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

所以,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');

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

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

在我看来这不是错误。

【讨论】:

  • 但是,一旦setTimeout() 回调运行,setTimeout() 回调的函数范围就完成了,整个范围应该被垃圾回收,释放它对some 的引用。不再有任何可以运行的代码可以到达闭包中的some 实例。它应该被垃圾收集。最后一个例子更糟糕,因为unreachable() 甚至没有被调用,也没有人引用它。它的范围也应该是 GCed。这两个似乎都是错误。 JS 中没有语言要求来“释放”函数范围内的东西。
  • @some 它不应该。函数不应该关闭它们没有在内部使用的变量。
  • 它可以被空函数访问,但它不是,所以没有对它的实际引用,所以应该很清楚。垃圾收集跟踪实际引用。它不应该保留可以引用的所有内容,而应该保留实际引用的内容。一旦最后一个f() 被调用,就不再有对some 的实际引用。它无法访问,应该被 GCed。
  • @jfriend00 我在(标准)[ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf] 中找不到任何关于只有它内部使用的变量应该可用的内容。在第 13 节中,生产步骤 4:让闭包成为创建 13.2 中指定的新 Function 对象的结果,10.2“外部环境引用用于建模词法环境值的逻辑嵌套。 (内部)词法环境的外部引用是对在逻辑上围绕内部词法环境的词法环境的引用。”
  • 嗯,eval 确实是个特例。例如,eval 不能为别名 (developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…),例如var eval2 = eval。如果使用了eval(因为它不能用不同的名字来调用,这很容易做到),那么我们必须假设它可以使用范围内的任何东西。
【解决方案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);

现场直播here

我希望使用最少的内存得到一个包含 500 个function() {} 的数组。

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

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

删除任一注释行可以解决所有问题。

似乎所有这三种浏览器(Chrome、Firefox 和 IE)都按上下文而不是按闭包保留环境记录。 Boris 推测这个决定背后的原因是性能,这似乎是可能的,尽管我不确定根据上述实验它的性能如何。

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

function g() { some; }

我用

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

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

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

附:出于好奇,我在 Java 中尝试了这个(利用它在函数内部定义类的能力)。 GC 就像我最初希望 Javascript 一样工作。

【讨论】:

  • 我认为外部函数 var g = (function(some) { return function() { some; }; } )(some); 缺少右括号;
  • 不知道最新的JS引擎是否仍然如此?
【解决方案3】:

启发式方法各不相同,但实现这种事情的一种常见方法是为您的情况下对f() 的每次调用创建一个环境记录,并且只存储实际关闭的f 的本地变量(由 一些闭包)在那个环境记录中。然后在对f 的调用中创建的任何闭包都会使环境记录保持活动状态。我相信至少 Firefox 是这样实现闭包的。

这具有快速访问封闭变量和实施简单的好处。它具有观察到的效果的缺点,即关闭某个变量的短期闭包会导致它通过长期闭包保持活动状态。

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

【讨论】:

  • 感谢您的洞察力。我得出结论,这也是 Chrome 实现闭包的方式。我一直认为它们是以后一种方式实现的,其中每个闭包只保留它需要的环境,但事实并非如此。我想知道创建多个环境记录是否真的那么复杂。与其聚合闭包的引用,不如将每个闭包视为唯一的闭包。我猜想性能考虑是这里的原因,但对我来说,共享环境记录的后果似乎更糟。
  • 后一种方式在某些情况下会导致需要创建的环境记录数量激增。除非您尽可能地尝试跨功能共享它们,否则您需要一堆复杂的机器来做到这一点。这是可能的,但有人告诉我,性能权衡有利于当前的方法。
  • 记录数等于创建的闭包数。我可能将O(n^2)O(2^n) 描述为爆炸式增长,但不是成比例的增长。
  • 嗯,与 O(1) 相比,O(N) 是一个爆炸式增长,尤其是当每个都可以占用相当多的内存时......再次,我不是这方面的专家;在 irc.mozilla.org 上的 #jsapi 频道上询问可能会比我提供的权衡取舍得到更好、更详细的解释。
  • @Esailija 不幸的是,这实际上很常见。您所需要的只是函数中的一个大临时变量(通常是一个大型类型数组),一些随机的短期回调使用和一个长期闭包。对于编写网络应用程序的人来说,最近出现了很多次......
【解决方案4】:
  1. 在函数调用之间维护状态 假设您有函数 add() 并且您希望它将在多次调用中传递给它的所有值相加并返回总和。

喜欢 添加(5); // 返回 5

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

添加(3); // 返回 28 (25 + 3)

您可以首先执行此操作的两种方法是正常定义一个全局变量 当然,您可以使用全局变量来保存总数。但请记住,如果你(ab)使用全局变量,这个家伙会把你活活吃掉。

现在最新的方式使用闭包没有定义全局变量

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

【讨论】:

    【解决方案5】:

    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

    【讨论】:

    • 请描述答案
    【解决方案6】:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-10-26
      • 2016-09-19
      • 2013-12-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多