JavaScript是一种隐蔽的功能性编程语言,其函数是闭包(封闭):函数对象可以访问其封闭作用域中定义的变量,即使该作用域已经完成。 一旦它们定义的函数已经完成,并且在其作用域内定义的所有函数本身都被 GCed(垃圾收集),那么由闭包捕获的局部变量就被垃圾收集。
var run = function () { var str = new Array(1000000).join(‘*‘); var doSomethingWithStr = function () { if (str === ‘something‘) console.log("str was something"); }; doSomethingWithStr(); }; setInterval(run, 1000);
我们将每秒执行一次run
函数。它将分配一个巨大的字符串,创建一个使用它的闭包,调用闭包并返回。返回后,闭包可以被垃圾收集,str也可以,因为没有什么引用它。但是,如果我们有一个闭包比run久的呢?
var run = function () { var str = new Array(1000000).join(‘*‘); var logIt = function () { console.log(‘interval‘); }; setInterval(logIt, 100); }; setInterval(run, 1000);
每隔一秒run
分配一个巨大的字符串,并开始每隔100微秒记录日志一次。logIt
永远都会持续,str
在其词法作用域内,所以这可能造成内存泄漏!幸运的是,JavaScript实现(或至少是现在的Chrome)足够聪明可以注意到在logIt
中没有使用str,所以它不会被放在logIt
的词法环境中,而且一旦运行完成,大字符串会被垃圾回收。
var run = function () { var str = new Array(1000000).join(‘*‘); var doSomethingWithStr = function () { if (str === ‘something‘) console.log("str was something"); }; doSomethingWithStr(); var logIt = function () { console.log(‘interval‘); } setInterval(logIt, 100); }; setInterval(run, 1000);
在Chrome开发者工具中打开“时间轴”选项卡,切换到内存视图,并点击记录:
看起来我们每秒多用额外的兆字节。甚至点击垃圾桶图标手动清理垃圾回收也没有帮助,所以看起来正在泄漏str。(译注:测试了新版chrome
59.0.3071.115(正式版本)这里好像没有出现泄漏)
但是这不是和以前一样吗?str
仅在run
函数体中引用,在doSomethingWithStr
函数中引用。一旦run
结束 doSomethingWithStr
本身就被清理掉。唯一从run
中泄漏的是第二个闭包,logIt
. 而 logIt
根本没引用str!
所以即使没有任何代码再次引用str,它也不会被垃圾回收器回收。为什么?典型的闭包实现是每个函数对象有一个链接到一个表示它词法作用域的字典类型对象。如果定义在run 函数中的两个函数的确用到str,重要是即使str被分配了一次又一次,这两个函数共享相同的词法环境。现在,Chrome的V8 javascript引擎中,如果变量(如例子中的字符串)没有被闭包引用时,可以将变量保留在词法环境之外,这就是第一个例子没有泄漏的原因。
但是只要变量被任何闭包使用,它将出现在在该作用域所有闭包共享的词法环境中。
你可以想象一个更聪明的词法环境实现来避免这个问题。每个闭包可以有一个读取和写入包含变量的字典(译注:对象);该字典中的值是可以 在多个闭包的词法环境中共享的变异元()。基于我对ECMAScript第5版标准的随意阅读,这是合法的:它对词汇环境的描述将其描述为“纯规范机制(不需要对应于ECMAScript实现的任何具体的文件)”。也就是说,这个标准实际上并不包含“垃圾”一词,只能说“内存”一次。
一旦你注意到这种形式的内存泄漏,修复它们是直接的,如修复Meteor 缺陷中演示的(the fix to the Meteor bug.)。在上面的例子中,很明显,我们有意泄漏logIt
而不是str。在原始的Meteor bug,我们不打算泄漏任何东西:我们只想用一个新对象代替一个对象,且允许先前的版本被释放,如下所示:
var theThing = null; var replaceThing = function () { var originalThing = theThing; // Define a closure that references originalThing but doesn‘t ever actually get called. //定义一个引用originalThing但实际上没有调用它的实例闭包 // But because this closure exists, originalThing will be in the // lexical environment for all closures defined in replaceThing, instead of // being optimized out of it. If you remove this function, there is no leak. //但是由于这个闭包的存在,originalThing将存于定义在replaceThing中的所有闭包的词法环境中,而不被优化,如果你移除 这个方法,就不会有泄漏 var unused = function () { if (originalThing) console.log("hi"); }; theThing = { longStr: new Array(1000000).join(‘*‘), // While originalThing is theoretically accessible by this function, it // obviously doesn‘t use it. But because originalThing is part of the // lexical environment, someMethod will hold a reference to originalThing, // and so even though we are replacing theThing with something that has no // effective way to reference the old value of theThing, the old value // will never get cleaned up! //虽然originalThing理论上可以通过这函数(someMethod)访问,但显然没有使用它,但是由于originalThing是词法环境的一部分,someMethod将会保留一个引用指向originalThing,所以即使我们用非有效的方式替换旧的theThing值,但是旧值不会被清理。 someMethod: function () {} }; // If you add `originalThing = null` here, there is no leak. //此处加originalThing = null不会有泄漏 }; setInterval(replaceThing, 1000);
总结一下:如果你有大对象被一些闭包使用, 而(译注:这些闭包)不是任何你需要继续使用的闭包,只要确保当你用大对象的时候,局部变量不再指向它。不幸的是,这些错误可能非常巧妙的(不好发现); 如果JavaScript引擎不需要您考虑它们会更好一些。
原文地址: http://point.davidglasser.net/2013/06/27/surprising-javascript-memory-leak.html
相关: http://mrale.ph/blog/2012/09/23/grokking-v8-closures-for-fun.html