170

我记录了以下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

6 回答 6

78

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

来自 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函数中是否使用了 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_

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

于 2013-11-05T23:02:30.580 回答
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);

现场直播

我希望以 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);

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

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

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

于 2013-11-06T02:45:30.387 回答
16

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

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

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

于 2013-11-06T04:09:27.067 回答
0
  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));
  
}());

于 2018-09-12T03:30:38.930 回答
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

于 2018-09-12T04:11:42.450 回答
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
    
  
}());

于 2018-09-12T04:26:36.930 回答