如何对 JavaScript 闭包进行垃圾收集

我已经记录了下面的 铬虫,它导致了我的代码中许多严重和不明显的内存泄漏:

(这些结果使用 Chrome 开发工具的 内存剖析器内存剖析器,它运行 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() {})似乎可以保持所有对象“活动”,无论该闭包本身是否可访问。

我的问题是关于其他浏览器(IE9 + 和 Firefox)中闭包的垃圾收集。我非常熟悉 webkit 的工具,比如 JavaScript 堆分析器,但是我对其他浏览器的工具知之甚少,所以我还不能测试它。

在这三种情况中,IE9 + 和 Firefox 垃圾将收集 someClass < strong > 实例?

21349 次浏览

据我所知,这不是一个 bug,而是预期的行为。

来自 Mozilla 的 内存管理页面: “从2012年开始,所有现代浏览器都推出了标记-清除(mark-and-clear)垃圾收集器。”。

在失败的例子中,some仍然可以在闭包中到达。我尝试了两种方法让它变得遥不可及,两种方法都奏效了。要么你设置 some=null时,你不再需要它,或者你设置 window.f_ = null;,它将消失。

更新

我在 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_分配另一个值而中断对对象的引用,则释放内存。

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

我在 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() {}的数组。

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

Chrome 最终停止运行并关闭,Firefox 在使用了近4GB 内存后完成整个过程,而 IE 的增长渐近放缓,直到显示“内存不足”。

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

似乎所有这三个浏览器(Chrome、 Firefox 和 IE)都保持每个上下文的环境记录,而不是每个闭包的环境记录。鲍里斯假设这个决定背后的原因是性能,这似乎是可能的,虽然我不确定如何表现它可以被称为根据上述实验。

如果需要一个引用 some的闭包(假设我没有在这里使用它,但假设我使用了) ,如果

function g() { some; }

我吸毒

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

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

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

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

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

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

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

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

喜欢 Add (5) ;//返回5

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

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

第一种方法是定义一个 < strong > 全局变量 当然,您可以使用全局变量来保存总数。但是要记住,如果你使用全局变量,这个家伙会把你生吞活剥的。

现在最新的方式 做个了结没有定义 全局变量

(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

(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
    

  

}());