我记录了以下 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();
以及相应的截图:
https://i.stack.imgur.com/ZSVf0.png
如果对象被同一上下文中的任何其他闭包引用,则闭包(在本例中为 function() {}
)似乎使所有对象保持“活动”,无论该闭包本身是否可访问。
我的问题是关于其他浏览器(IE 9+ 和 Firefox)中闭包的垃圾收集。我对 webkit 的工具非常熟悉,比如 JavaScript 堆分析器,但是我对其他浏览器的工具知之甚少,所以我无法对此进行测试。
在这三种情况中,IE9+ 和 Firefox 会垃圾收集 someClass
实例中的哪一种?
unreachable
函数从未执行,因此实际上没有记录任何内容。
据我所知,这不是错误,而是预期的行为。
来自 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 节词法环境:
“(内部)词汇环境的外部引用是对在逻辑上围绕内部词汇环境的词汇环境的引用。外部词汇环境当然可以有自己的外部词汇环境。词汇环境可以作为外部词汇环境多个内部词法环境的环境。例如,如果函数声明包含两个嵌套函数声明,则每个嵌套函数的词法环境将具有当前执行周围函数的词法环境作为它们的外部词法环境。
因此,一个函数将可以访问父级的环境。
因此,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);
实时网站 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);
它将通过将闭包移动到与我的其他函数不同的上下文来解决内存问题。
这会让我的生活更加乏味。
PS出于好奇,我在Java中尝试了这个(利用它在函数内部定义类的能力)。 GC 就像我最初希望的 Javascript 一样工作。
启发式方法各不相同,但实现此类事情的常用方法是为您的情况下对 f()
的每次调用创建一个环境记录,并仅存储实际关闭的 f
的本地变量(由 some 关闭)在该环境记录中。然后在对 f
的调用中创建的任何闭包都会使环境记录保持活动状态。我相信这至少是 Firefox 实现闭包的方式。
这具有快速访问封闭变量和实现简单的好处。它具有观察到的效果的缺点,即关闭某个变量的短期闭包会导致它通过长期闭包保持活动状态。
可以尝试为不同的闭包创建多个环境记录,具体取决于它们实际关闭的内容,但这会很快变得非常复杂,并可能导致其自身的性能和内存问题......
O(n^2)
或 O(2^n)
描述为爆炸,但不是成比例的增加。
在函数调用之间保持状态假设您有函数 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("确保国家电话");返回函数状态(){ var totalstate = 0; if(totalstate==0){ console.log("makesure statecall");返回函数(val){总状态+= val; console.log("你好:"+totalstate);返回总状态; } }else{ console.log("hey:"+totalstate); } }; }; var CA=国家();变种 ST=CA(); ST(5); //我们添加了 5 个状态 ST(6); //几年后,我们需要添加新的 6 个状态,所以现在总共 11 个 ST(4); // 15 var CB=Country(); var STB=CB();机顶盒(5); //5 机顶盒(8); //13 机顶盒(3); //16 var CX=国家; var d=国家();控制台.log(CX); //在CA console.log(d)中存储为国家的副本; //在国家函数中作为返回值存储在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 }());
不定期副业成功案例分享
setTimeout()
回调运行,setTimeout()
回调的函数范围就完成了,整个范围应该被垃圾回收,释放它对some
的引用。不再有任何可以运行的代码可以到达闭包中的some
实例。它应该被垃圾收集。最后一个例子更糟糕,因为unreachable()
甚至没有被调用并且没有人引用它。它的范围也应该是 GCed。这两个似乎都是错误。 JS 中没有语言要求来“释放”函数范围内的东西。f()
被调用,就不再有对some
的实际引用。它是不可访问的,应该被 GCed。eval
确实是特殊情况。例如,eval
不能是别名 (developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…),例如var eval2 = eval
。如果使用了eval
(并且因为它不能用不同的名称调用,这很容易做到),那么我们必须假设它可以使用范围内的任何东西。