nodejs 内存泄露问题?

`

var theThing = null
var replaceThing = function () {
    var originalThing = theThing
    var unused = function () {
        if (originalThing)
        console.log("hi")
     }
    theThing = {
        longStr: new Array(1000000).join('*'),
        someMethod: function () {
          console.log('someMessage')
        }
    };
};
setInterval(replaceThing, 1);`

描述问题:
这是nodejs中一个典型的垃圾回收案例,上面那段代码运行之后会带来非常明显的内存泄露情况,问题在于这段代码并没有形成闭包,为什么堆内的对象没有被立即释放?
原文中的一个解释是unused函数内部引用了originalThing,但是函数运行之后,申请的内存就会被释放,原本指向unused的函数的指针也就没有了,所以unused函数在堆中也应该会被回收,对originalThing的引用也就不存在了,那么怎么会引起内存泄露呢?

阅读 5.1k
6 个回答

这段代码做了一件事:每次调用 replaceThing 时,theThing 都会得到新的包含一个大数组和新的闭包(someMethod)的对象。同时,没有用到的那个变量持有一个引用了 originalThing(replaceThing 调用之前的 theThing)闭包。关键的问题是每当在同一个父作用域下创建闭包作用域的时候,这个作用域是被共享的。在这种情况下,someMethod 的闭包作用域和 unused 的作用域是共享的。unused 持有一个 originalThing 的引用。尽管 unused 从来没有被使用过,someMethod 可以在 theThing 之外被访问。而且 someMethod 和 unused 共享了闭包作用域,即便 unused 从来都没有被使用过,它对 originalThing 的引用还是强制它保持活跃状态(阻止它被回收)。当这段代码重复运行时,将可以观察到内存消耗稳定地上涨,并且不会因为 GC 的存在而下降。本质上来讲,创建了一个闭包链表(根节点是 theThing 形式的变量),而且每个闭包作用域都持有一个对大数组的间接引用,这导致了一个巨大的内存泄露。

每次执行theThing 都是一个全新的对象,和之前的引用没有关系。
但是theThing 的作用域更广,函数运行完毕之后还在。
下一次执行时originalThing 指向上一次的theThing的值。

关键的一点,unused 函数内部引用了originalThing,也就是上一次的theThing。 这就打通了一个外部访问函数内部值的通道。由于函数unused 内部引用了originalThing,而originalThingtheThing相通,因此这一段内存不能释放。

代码一会在执行,这块内存就越堆越多了。

我的简单理解,内存泄漏就是两者相互引用,只要还有任意一个引用关系存在,就不能释放这段内存。

算是对@王琰 王琰 的补充吧:

图片描述

可以看出,每次调用replaceThing都会产生一个someMethod闭包,someMethod闭包里有originalThing,里面又有上次的someMethod……

但是someMethod的代码并没有直接引用originalThing,但是和unused共享了context,就引用了originalThing。

也就和下面的的代码差不多了:

var theThing = null;
var replaceThing = function () {
    var originalThing = theThing;
    theThing = {
        longStr: new Array(1000000).join('*'),
        someMethod: function () {
            if (originalThing)
            console.log("hi")
        }
    };
};
setInterval(replaceThing, 1000);

共享作用域/context这点有点违反我的直觉……后来想想因为变量都是可变的,共享才是合理的。

这个不会引起内存泄漏呀,但是如果说内够搞跨nodejs还是有可能的。因为 1ms 毕竟很短

显式地调用会用应该用
delete variable;
设置 vaiable = null; 的话并不会立即回收, 而是遵从默认的回收机制( 回收是有时间间隔的 )

所以你这个程序之所以会造成泄露应该也是短时间生产大量垃圾造成的, 至于为什么没有设置阈值, 达到一定量就自动回收这个问题...我猜应该不是开发组对回收的遗漏, 而是他们自己预估了一下正常情况下可能面临的最大压力, 不会引起泄露


呀, 算了, 我瞎猜的.233

整理了一下现有几个答案,做一些小的补充。

首先,每个闭包在创建时,都会保留当时作用域中的完整 Variable Object 的引用

因此尽管 theThing.someMethod 并未使用 originalThing,但因为与 unused 共享作用域,因此其闭包中仍然存在该变量。

接下来看表格:

运行次数 theThing originalThing
0. null undefined
1. Object(闭包 originalThing) null
2. Object(闭包 originalThing * 2) Object(闭包 originalThing)
3. Object(闭包 originalThing * 3) Object(闭包 originalThing * 2)
...

我用 * n 来粗略表达其引用关系,不太准确,但应该可以看懂。

可以看到,每次运行,theThing 都会创造一个包含 originalThing 引用的闭包。尤其值得注意的是,自第2次运行开始,originalThing 本身又指向了上一次运行后的 theThing,结果就如 @王琰 ,@oraoto 答案中所述,theThing 所创建的闭包成为了一个巨大的链表结构,不断向上引用到最初的值。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题