JavaScript 闭包如何垃圾回收

IT技术 javascript internet-explorer google-chrome firefox garbage-collection
2021-01-30 10:59:55

我记录了以下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 实例中的哪一种?

6个回答

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

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

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

更新

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

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

  • 第 13 节函数定义,第 4 步:“让闭包成为创建 13.2 中指定的新函数对象的结果”
  • 第 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');

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

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

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

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

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

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

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

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

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

function g() { some; }

我用

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

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

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

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

我想知道最新的 JS 引擎是否仍然如此?
2021-03-30 10:59:55
我认为外函数没有右括号 var g = (function(some) { return function() { some; }; } )(some);
2021-04-04 10:59:55

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

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

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

在某些情况下,后一种方式会导致需要创建的环境记录数量激增。除非您尽可能努力地跨功能共享它们,否则您需要一堆复杂的机制来做到这一点。这是可能的,但我被告知性能权衡有利于当前的方法。
2021-03-26 10:59:55
记录数等于创建的闭包数。我可能将O(n^2)描述O(2^n)为爆炸,但不是按比例增加。
2021-03-26 10:59:55
感谢您的见解。我得出的结论是,这也是 Chrome 实现闭包的方式。我一直认为它们是后一种方式实现的,每个闭包只保留它需要的环境,但事实并非如此。我想知道创建多个环境记录是否真的那么复杂。与其聚合闭包的引用,不如将每个闭包视为唯一的闭包。我已经猜到性能考虑是这里的推理,尽管对我来说共享环境记录的后果似乎更糟。
2021-04-06 10:59:55
@Esilija 不幸的是,这实际上很常见。您所需要的只是函数中的一个大型临时对象(通常是大型类型数组),一些随机的短期回调使用它和一个长期存在的闭包。对于编写网络应用程序的人来说,最近出现了很多次......
2021-04-07 10:59:55
好吧,与 O(1) 相比,O(N) 是一个爆炸式增长,尤其是当每个人都可以占用相当多的内存时……同样,我不是这方面的专家;在 irc.mozilla.org 上的 #jsapi 频道上询问可能会为您提供比我所能提供的权衡更好、更详细的解释。
2021-04-08 10:59:55
  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));
  
}());

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

请描述答案
2021-04-08 10:59:55