js的闭包和回调到底怎么才会造成真的内存泄漏呢?

最近看了一些分享的js内存泄漏典型案例,感觉这种代码以前一直写,现在不留意的话也一直这么写,到现在发现写代码的时候老怕闭包和回调了,在前端出现泄露的话问题还不算非常严重,但如果后端使用nodejs写出现内存泄露的话,这可会让服务崩溃。
为了严谨性,到这里问下各位技术达人。

//一下是angularjs的controller模拟
function($scope){
    $scope.todo1 = todo1;
    $scope.todo2 = todo2;
    $scope.todoAll = {
          todo1 : todo1..
      }
}

像这样的回调其实就是一种闭包,那么这个时候会造成内存泄露吗?
再看这里


function foo($scope){ var text = 'you click me!!!'; var clickHanlder = function(){ alert(text); } $scope.clickHanlder = clickHanlder; } var $a = {} foo($a); $a.clickHanlder(); //这里的text被提到全局的作用域了,这种算内存泄露吗?如果$a是DOM呢?
var gloablObject = {}
var addCallback = function(hanlder){
    //这样的闭包回调有问题吗?
    var value = function(){
        var temp;
        if(true){
            temp = new Value(true);
        }else{
            temp = new Value();
        }
        return temp;
    }

    hanlder(value);
}
addCallback(function(value){
    //1 这里有问题吗?
    globalObject.ref = value;

    //2 这里呢?
    globalObject.ref = function(nv){
        alert(nv.todo(value));
    }

});

还有

//下面的代码大致模拟一下angularjs的写法
fn(function($add){
    var pro1 = 'pro1';
    var pro2 = 'pro2';
    return function(){
        $add(pro1,pro2);
    }
});

类似这些写法很经常,现在遇到闭包和回调我都分不出我写的代码会不会造成内存泄露了,js坑很大啊!!

阅读 31.1k
4 个回答

兄弟,不必担心。
首先,能导致内存泄漏的一定是引用类型的变量,比如函数和其他自定义对象。而值类型的变量是不存在内存泄漏的,比如字符串、数字、布尔值等。
因为值类型是靠复制来传递的,而引用类型是靠类似c语言中的指针来传递的。
可以认为一个引用类型的变量就是一个指向某个具体的内存地址的指针

当我们用js代码创建一个引用类型的时候(以下简称对象),js引擎会在内存中开辟一块空间来存放数据,并把指针引用交给那个变量。内存是有限的,js引擎必须保证当开辟的对象没用的时候,把所分配的内存空间释放出来,这个过程叫做垃圾回收,负责回收的叫做垃圾回收器(GC)。

OK,内存泄漏是指我们已经无法再通过js代码来引用到某个对象,但垃圾回收器却认为这个对象还在被引用,因此在回收的时候不会释放它。导致了分配的这块内存永远也无法被释放出来。如果这样的情况越来越多,会导致内存不够用而系统崩溃。

不可控的东西才是最可怕的!

最经典的例子就是外部我们不可控的引用。比如说IE6中dom对象引用了js对象,而dom对象在某个时刻被移除掉了,但js引擎不知道它被移除掉,还傻傻的保留着引用呢,就不会把js对象释放。(ie7+改善很多了,我不是黑IE)

然后就是闭包中的引用了。咱们使用闭包的目的,就是要保存内部变量的状态以便我们哪个时候去通过闭包使用它作用域内的变量。

我们可以把闭包形象的理解为一道门,屋子里面是内部变量。钥匙是一个引用。
当我们把钥匙给张三这个对象(otherObject1.p1 -> 门),产生了一个引用
当我们再配一把钥匙给李四这个对象(otherObject2.p2 -> 门)产生了另外一个引用

GC在回收的时候会判断一个闭包还有没有人拿着钥匙,要是没有引用或者是内部循环引用(李四在屋子里),就会释放闭包内变量所在的空间,回收垃圾

我斗胆的说一句:严格意义上讲,闭包不是真正产生内存泄漏的原因!各位有意见可以评论里指出,现在举个最简单的例子:

function bindEvent() 
{ 
    var obj = document.createElement("XXX"); 
    obj.onclick = function(){ 
        // ... 
    } 
}

bindEvent();

这人把钥匙(引用)给了一个外部不可控的dom对象,怎么能怪人家闭包的错误呢!
再看下面代码:

var otherJsObj = {};
function bind() 
{ 
    otherJsObj.func1 = function(){ 
        // ...
    } 
}

bind();

我把钥匙给了otherJsObj。然后叮嘱它:“你不用的时候就把你的func1置空或者赋值成别的对象,解除我的引用,我好回收垃圾”。这样可控,因为咱们都是自己人(js对象),有访问权限[呵呵]

楼主会产生这样的困惑,是因为闭包确实是在保持对别的对象的引用。也会产生较大的内存占用。但这是可控制的,不是闭包的错。
谢谢!

@李维 把道理讲述的很清楚了,我觉得说得很棒,也很认同他对必包的看法。我想补充一些实战性的建议。

首先,你要清楚一件事情:绝大部分的内存泄露都不是有 Javascript 自身引起的,Javascript 的 GC 已经做的相当不错了(或者说浏览器引擎的 GC,比如 webkit),大多数内存泄露总是源自于和外部 API 的交互,特别是 DOM,以及一些 HTML(5)提供的外部 API。我们都知道传统意义上的 Javascript 其实包含了 ECMAScript+BOM+DOM+Maybe Others,而经典的工作场景又总是在混合环境下(浏览器),所以一出现内存泄露就把屎盆子往 ECMAScript 身上扣是不道德的,而且一出现内存泄露就去找代码纯粹属于 ECMAScript 部分的问题也是盲目的(当然不是没有丝毫可能性)。

如果你确实觉得没底,那么本着务实的精神,首先看一下自己是否遵从了下列原则(欢迎补充):

  1. 由于我们的 JS 代码大体上都是依靠事件来驱动的,并且事件基本上都绑定在 DOM 对象上,因此要尽可能的利用 Delegation,把相关联的事件绑定在同一个 DOM 对象上,要记得善加利用事件的冒泡和传播特性。因为我们使用事件不光是为了绑定和监听,更重要的是使用大量的回调函数。如果这些事件乱绑一气,又不会在合适的时刻对它们解绑或者把宿主销毁,那么这些回调函数的引用就会一直被保持着等待触发执行。如果这些函数之中还有必包引用着大量的没有被销毁处理的 DOM 对象……你就可想而知了。

  2. 可复用的对象(特别是 DOM 对象)应及早将其引用赋给变量,并保持使用该变量来进行进一步的操作,包括向下的遍历查找,插入、复制新对象等等,这样一来 GC 回收会变得更加高效。如果你能很好的设计应用的流程和交互,那么基本上这些对象都能在恰当的时刻被主动销毁,比如说附个 null 给它的引用,一旦对象没有了任何引用,GC 会很快将其加入销毁队列。哪怕之后还要重新创建它,多数时候也是有益的,你可以通过观测性能指标(后面讲)或者分析用户的行为来找到一个合理的平衡。

    1. 为什么 jQuery 的教程总是说:请把 jQuey 对象尽早的使用变量保存起来,而不是反复的使用 $(sameSelector)?道理就在此。
    2. 基于对开始的第一条原则,保存一个可复用的上层 DOM 对象,可以使得其 DOM 树自节点需要的事件监听都绑定在父节点身上。
  3. 触发事件的时候别一股脑的都往默认事件类型身上丢,jQuery 提供了很好的事件命名空间机制,要善于利用。比如说 $(selector).on('click.my_event', fn),要比 $(selector).on('click', fn) 漂亮得多。Why?因为你可以随时 $(selector).off('click.my_event'),而 click 你就不敢随便解绑了,因为鬼知道还有谁在监听它?

  4. 同样的道理,使用命名函数也要比大量使用匿名函数要合理得多,还是上面那个例子,即使你非要用 click,那也不要用匿名函数,否则解绑的时候你怎么去指明要解绑的回调是哪个?

  5. IIFE 是安全的,它永远不会引起内存泄漏,因为没人引用它。

  6. 为什么开发者那么偏爱 Google Chrome?因为在开发者工具里,Chrome 的 Timeline(里面有 Memory Analyzer)和 Profiles 工具是做的最棒的!学会使用它,你可以定位和分析一切造成内存泄露的所在及原因,成功的使用它几次,你就会变成内存管理专家的。

这里有一篇非常棒的入门介绍,作者是 Google 的小神(真大神都不带露面的):http://addyosmani.com/blog/taming-the-unicorn-easing-javascript-memory-profiling-in-devtools/

这里有 Gmail 团队写的探查和解决内存的演讲稿(可能要翻墙):https://docs.google.com/presentation/d/1wUVmf78gG-ra5aOxvTfYdiLkdGaR9OhXRnOlIcEmu2s/pub?start=false&loop=false&delayms=3000#slide=id.g14717ff3_0_23

实践出真知,学好这些工具,多做实验,有些理论性的东西靠想或是靠别人说都是徒劳,自己发现之后一下就醒悟了。


补充:抱歉,写完了回头一看,题主好像主要关心的是 Node.js,而我之前多数都是和浏览器有关的,所以再补充一点。

其实 Node.js 用的是 V8 引擎嘛,和 chrome 一样的,虽然不是太了解底层有没有对其做出什么改变,但是 GC 相比只会更好不会更差吧?这样一来,问题反而更小了不是么。

有一个比较经典的范例我上面简单提到过,就是强制 GC 去回收你指定的对象,基本上就是这样:

var someFn = function (foo, cb) {
    var veryBigObject = new veryBigObject();
    doSomeWith(foo).on('event', function (e) {
        // 做该做的事
        cb(veryBigObject);    // 在某处用了大对象,也不一定非得是回调传参,举例而已
        // 做该做的事
        veryBigObject = null;    // V8! 请你回收它,谢谢!
    }
}

就是这样了。或许你的应用场景不尽如此,但是原则是一样的:只要你没谱,就在恰当的时候 null 了它。(或者先观察,找出可能存在泄露的地方再这么做)

再附上 Mozilla 上看到的一篇 Nodejs 处理内存泄露的文章(可能有点过时):https://hacks.mozilla.org/2012/11/tracking-down-memory-leaks-in-node-js-a-node-js-holiday-season/

另外还有一篇非常经典的分析典型案例的文章(题主是不是看的这个?):http://www.ibm.com/developerworks/web/library/wa-memleak/

看过一篇文章,提到javascript内存泄露的成因。
由于引用计数的使用,实际上应该不会出现真正的内存泄露,出现“从代码上分析允许访问”但“程序实际上永远不会访问”的这种“泄露”是有可能的。
但是,写这篇文章的时候(2008年),所有浏览器的内存中,引用计数都是分开进行的:javascript引擎和dom。两部分内存空间中的相互引用有可能导致内存泄露。
特别是使用iframe的时候泄露非常明显,由于js和dom都不能gc,所以内存占用增长非常快。

建议读javascript权威指南

推荐问题
宣传栏