javascript中的关闭和回调内存泄漏

Posted

技术标签:

【中文标题】javascript中的关闭和回调内存泄漏【英文标题】:Closure and callback memory leak in javascript 【发布时间】:2013-05-02 18:33:53 【问题描述】:
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 的回调使用从该回调中定义的变量应用程序。从 mongodb 获得结果后,我也将它作为回调返回(因为它都是异步的,我不能只写 return )。所以实际上可能会发生我将回调一直传播到源......

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 实例,以便处理程序可以比较类型。您将其实例化一次,这样每次处理程序运行时都会减少负载。如果你想清理它,每次都在处理程序中实例化它,或者期望它“泄漏”内存,因为它就是这样工作的。 用 .one() 代替 .on() 怎么样? 请注意,我不使用 jQuery,也不绑定或注册某些事件。我只是将回调函数传递给另一个。我使用 NODE.JS ! 我担心的是,我可能有一系列回调......例如,业务函数调用数据函数调用 mongodb,并且这些函数之间的所有通信都在回调中。也许来自 mongodb 的回调确实可以清除所有内容,但业务和数据层中的这些其他回调是什么。我想 GC 只看到回调,而他看不到他下面的东西。我还没有对泄漏进行分析,但我可以清楚地看到我的 node.js 工作人员使用了太多的 rss(RAM) 内存。 是的,但是在每一种回调中(在业务层或数据层)你都可以有一个闭包,你真的确定他们会清理吗? ...听听这个例子。在代码的其他部分中,我也从数据库中读取但在流中。我有 stream.on(data)... 事件,每次当我收集 1000 条记录时,我都会为业务层启动回调(在我的第一篇文章中的示例中,它将是这个:回调(结果))...业务层回调函数的实现实际上是一个闭包,它工作得很好。为什么 GC 在第一次回调后没有清理该闭包? 【参考方案1】:

以 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);
    )

【讨论】:

【参考方案2】:

如果你的回调函数应该只被调用一次,那么你应该在它被调用后取消订阅。这将释放你的回调 + 对 GC 的闭包。随着您的关闭发布,bigObject 也将免费被 GC 收集。

这是最好的解决方案 - 正如您所指出的,GC 不会神奇地知道您的回调只会被调用一次。

【讨论】:

感谢您的回答,但您能举一些取消订阅回调的例子吗? 这取决于您使用什么机制来订阅回调。您没有提供足够的详细信息来说明 doFoo() 的作用。大多数框架要么提供undoFoo() 方法,要么doFoo() 本身返回一个您可以调用以取消订阅的方法。如果您使用 jQuery,您将使用 $("...").on("change", cb); 订阅并使用 $("...").off("change", cb) 取消订阅。事实上,他们有一种方法可以针对您只想订阅 1 个事件的确切情况:$("...").one("change", cb),它会在一次调用后自动取消订阅。 请注意我在第一篇文章中更改了示例。这现在更像是我的实际问题。我将回调函数传递给另一个函数。我使用 node.js。 Foo 只是一个例子,但问题与我在现实世界中的功能相同。回调函数在范围内使用变量,GC 无法隐式清理它:( ...我的 rss 内存增长得非常快,因为我有很多流量。 问题是我不知道如何在这个特定的问题中从回调中注销!正如我所说,我使用 node.js 并且我没有绑定到像 jQuery 或类似的事件,我只是将回调函数传递给另一个函数(在这个例子中是 doFoo)并且 doFoo 做一些 IO 操作,然后在我传递的某个点调用回调函数。 Node.Js 没有名为 doFoo 的函数。如果你不想给我们一个具体的例子,那么我只能给出一般性的建议,基本上就是 RTFM。使用特定的 node.js 函数发布一个特定的示例,您将获得特定的建议。

以上是关于javascript中的关闭和回调内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

音频回调线程中的内存泄漏(iOS)

用 Rust 处理 WebAssembly 中的闭包而不是使用忘记和泄漏内存有啥更好的方法?

内存泄漏(Memory Leak)

对已关闭的视图控制器的异步回调?

JavaScript中的内存泄漏以及如何处理

检测代码中的 node.js/javascript 内存泄漏