js闭包泄漏-replaceThing问题(A surprising JavaScript memory leak found at Meteor)

 

JavaScript是一种隐蔽的功能性编程语言,其函数是闭包(封闭):函数对象能够访问其封闭做用域中定义的变量,即便该做用域已经完成。 一旦它们定义的函数已经完成,而且在其做用域内定义的全部函数自己都被 GCed(垃圾收集),那么由闭包捕获的局部变量就被垃圾收集。javascript

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久的呢?html

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的词法环境中,并且一旦运行完成,大字符串会被垃圾回收。java

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开发者工具中打开“时间轴”选项卡,切换到内存视图,并点击记录: git

 

看起来咱们每秒多用额外的兆字节。甚至点击垃圾桶图标手动清理垃圾回收也没有帮助,因此看起来正在泄漏str。(译注:测试了新版chrome 59.0.3071.115(正式版本)这里好像没有出现泄漏github

可是这不是和之前同样吗?str  仅在run函数体中引用,在doSomethingWithStr 函数中引用。一旦run 结束  doSomethingWithStr 自己就被清理掉。惟一从run 中泄漏的是第二个闭包,logIt . 而 logIt 根本没引用strchrome

 

因此即便没有任何代码再次引用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

 

附加总结:

1.每一个函数都有一个词法环境,编译时产生词法做用域。

2.函数执行时会产生Context上下文,这个上下文存储函数中的变量。

3.若是做用域自己嵌套在一个闭包中,那么新建立的Context 上下文将会指向父对象。 这可能会致使内存泄漏。---> 函数嵌套函数,内部函数将建立一个新上下文.

相关文章
相关标签/搜索