JavaScript 中的闭包和回调函数内存泄漏问题

20
function(foo, cb) {
  var bigObject = new BigObject();
  doFoo(foo, function(e) {
     if (e.type === bigObject.type) {
          cb();
          // bigObject = null;
     }
  });
}

以上示例展示了一个典型的、偶然(或者说不是)的内存泄漏闭包。V8垃圾回收器无法确定是否安全地移除bigObject,因为它在可以被多次调用的回调函数中使用。

一种解决方案是在回调函数的作业完成后将bigObject设置为null。但是如果你正在使用许多变量(想象一下有n个像bigObject这样的变量,并且它们都在回调中使用),那么清理这些变量会成为一个棘手的问题。

我的问题是:有没有其他方法来清理这些已使用的变量?

编辑 这里还有另一个(现实世界的)例子:所以我从mongodb获取应用并将其与其他应用程序进行比较。 mongodb的回调使用在该回调之外定义的变量application。在我从mongodb获取结果后,我也将其作为回调返回(因为所有都是异步的,我不能只写返回)。所以实际上可能会将回调传播到源头……

function compareApplications(application, condition, callback) {

    var model = database.getModel('Application');
    model.find(condition, function (err, applicationFromMongo) {
        var result = (applicationFromMongo.applicationID == application.applicationID)
        callback(result)        
    }
}

让我问你一下 - 为什么这是个问题?change处理程序应该被调用多次。那么,除非您取消绑定change事件,否则您(或GC)如何知道何时真正结束使用bigObject?您似乎想要一个bigObject实例,以便处理程序可以比较类型。您只需实例化一次,这样可以减少每次处理程序运行的负载。如果您希望它被清理,请在处理程序内部每次实例化它,或者期望它“泄漏”内存,因为这就是它的工作方式。 - Ian
1
请注意,我不使用jQuery,也不绑定或注册到某些事件。我只是将回调函数传递给另一个函数。我使用NODE.JS! - Ivan Longin
谢谢。我相信像model.find这样的方法应该会在异步操作完成后自动释放对回调函数的引用。这个小代码片段不应该泄漏任何东西(假设model.find没有错误)。你确定它真的在泄漏吗?你应该进行一些堆分析,看看哪些对象实际上正在泄漏,以及是什么保留了对它们的引用。 - Brandon
1
我关心的是我可能会有一系列的回调...例如业务函数调用数据函数,数据函数调用mongodb,所有这些之间的通信都在回调中。也许来自mongodb的回调确实清除了一切,但是业务和数据层中的其他回调怎么办?我想GC只看到回调,而他下面的东西他不看。我还没有运行泄漏分析,但我可以清楚地看到我的node.js工作进程正在使用太多rss(RAM)内存。 - Ivan Longin
1
是的,但在每种回调(在业务或数据层)中,您都可以有一个闭包,您真的确定它们会清理吗?听听这个例子。在代码的其他部分中,我还从数据库中读取数据,但是以流的形式。我有stream.on(data){...}事件,每当收集1000条记录时,我就会触发业务层的回调(在我的第一篇帖子中的示例中,它将是这个:callback(result))…业务层中回调函数的这种实现实际上是一个闭包,并且它运行得非常好。为什么GC没有在第一次回调后清理掉那个闭包呢? - Ivan Longin
显示剩余7条评论
2个回答

2
如果你的回调函数只需要被调用一次,那么在它被调用后就应该取消订阅。这将释放回调和闭包给 GC。当你的闭包被释放时,bigObject也会被 GC 释放。
这是最好的解决方案 - 正如您所指出的,GC 不会神奇地知道您的回调只会被调用一次。

2
谢谢您的回答,但您能举个取消回调订阅的例子吗? - Ivan Longin
1
请注意,我已更改第一篇帖子中的示例。现在更接近我的实际问题。我将回调函数传递给另一个函数。我使用node.js。Foo只是一个示例,但与我的真实世界函数中的问题相同。回调函数正在使用作用域中的变量,GC无法隐式清除它:(...因为我有很多流量,所以我的rss内存很快就会上升。 - Ivan Longin
3
问题在于我不知道如何在这个特定问题中取消回调函数的注册!正如我所说的,我使用的是node.js,并且我不像在jQuery中那样绑定事件,我只是将回调函数传递给另一个函数(在这个示例中是doFoo),并且doFoo执行一些IO操作,然后在某个时刻调用我传递的回调函数。 - Ivan Longin
1
Node.js没有名为doFoo的函数。如果您不想给我们一个具体的例子,那么我能给的只是一般性建议,基本上就是RTFM。发表使用特定node.js函数的具体示例,您将会得到具体的建议。 - Brandon
1
我在第一篇帖子中添加了一些真实世界的例子。 - Ivan Longin
显示剩余2条评论

0
继续Brandon的回答:如果(由于某些可怕的原因)您无法取消订阅回调,则始终可以自己处理删除回调:
function createSingleUseCallback(callback)
{
    function callbackWrapper()
    {
        var ret = callback.apply(this, arguments);
        delete callback;
        return ret;
    }
    return callbackWrapper;
}

function compareApplications(application, condition, callback)
{
    var model = database.getModel('Application');
    model.find(condition, createSingleUseCallback(function (err, applicationFromMongo)
    {
        var result = (applicationFromMongo.applicationID == application.applicationID);
        callback(result);
    })
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接