【问题标题】:node.js child process object lifecyclenode.js 子进程对象生命周期
【发布时间】:2021-06-04 16:29:14
【问题描述】:

通常,在函数中使用关键字“var”定义的 Javascript 变量应该是函数范围的。但我对与 node.js 子进程相关的情况感到困惑。我有两个文件,app.js(主文件)和child.js(子进程的执行脚本):

// app.js

const { fork } = require('child_process');

function bar() {
  var child = fork(`${__dirname}/child.js`); 

  child.on('message', msg => {
    console.log("fib(46) = " + msg);
  });
}

bar();
gc(); // force the garbage collection

// child.js,计算密集型任务,需要几十秒才能完成

function fib(n) {
  return n < 2 ? 1 : fib(n - 1) + fib(n - 2);
}

var result = fib(46);

process.send(JSON.stringify(result));

我期待这段代码可能会抛出一些异常,因为变量“child”是在函数 bar() 中创建的本地对象。当函数 bar() 完成执行时,它应该被垃圾收集。为接收子进程消息而注册的事件处理程序只是保存在'child'对象的哈希表中(子对象也是基于node.js子进程模块文档的EventEmitter的一个实例),所以这些哈希表也应该是垃圾作为被回收的本地“子”对象收集。

但真实情况是,几十秒后,当子进程计算完fib(46)并通过process.send返回结果时,父进程成功发出事件,最终触发注册的事件处理函数:

$ node --expose-gc .\app.js

fib(46) = 2971215073

那么,看起来子进程对象(由局部变量“child”指向的对象)仍在内存中?或者那个子进程对象是node.js子进程模块内部引用的,所以在子进程结束前不能被垃圾回收?我真的需要有人帮我澄清一下情况。

【问题讨论】:

    标签: javascript node.js garbage-collection fork child-process


    【解决方案1】:

    只有在没有其他代码可以访问或使用或引用该变量时,垃圾收集才能收集某些内容并回收其内存。有时当函数声明一个局部变量然后函数执行并返回时会发生这种情况,但并非总是如此。

    如果有异步操作正在运行(并且您的子进程是异步操作)并且在该函数范围内声明了事件处理程序或回调,并且仍然可以调用这些回调/事件,则该函数范围内的可访问变量不能是垃圾收集直到它们真正不再可访问,在您的情况下,可能在函数已经执行并返回之后很长时间,因为该生命周期与子进程的生命周期相关联,而不是与函数执行的生命周期相关联。

    在您的特定情况下,因为 child 对象仍然可以在函数返回后很长时间内发出事件,所以 child 对象及其事件处理程序可能访问的任何其他内容都不能被垃圾收集,直到子对象本身被 GC 或直到某些其他情况告诉垃圾收集器不能再调用该事件处理程序。

    因此,请记住,在 Javascript 中,函数中的局部变量不会进入严格的堆栈帧,该堆栈帧会在函数返回后立即 100% 回收。相反,它们是作用域对象上的属性,并且该作用域对象上的每个单独的属性只能在任何活动代码不再可访问时进行垃圾回收。因此,范围对象中的某些事物可能会被垃圾收集,因为它们没有在任何仍然可以调用的代码中被引用,而范围对象上的其他事物还不能被垃圾收集,因为在该函数范围内声明了回调可能仍会引用该函数中声明的一些变量。相同的规则适用于函数作用域或块作用域的变量,因为函数作用域只是比块作用域更大的块——它们现在的工作方式几乎相同。

    这是一个相当简单的例子:

    function run() {
        let total = 0;
        let msg = "About to start timer";
        console.log(msg);
    
        const timer = setInterval(() => {
            ++total;
            if (total >= 50) {
                console.log(total);
                clearInterval(timer);
            }
        }, 50);
    }
    
    run();
    

    在此代码中,只要函数 run() 退出,局部变量 msg 就可用于 GC。

    在调用 clearInterval() 并且最终计时器回调完成之前,变量 total 不可用于 GC。

    【讨论】:

    • 感谢您的快速回复。我理解你提供的例子。当然,函数seTinterval()会在dispatch queue中放入一个回调函数,回调函数通过闭包持有对局部变量total的引用。因为我们在调度队列中有引用这个变量的代码,所以它绝对不能用于 GC。但就我而言,v8 引擎如何知道我的子进程将调用 process.send 以返回一些消息并触发我的事件侦听器?
    • 如果我删除子进程脚本中的process.send(JSON.stringify(result)); 语句会怎样? V8 是否足够智能以跟踪所有内容,或者只是保守地防止本地子进程对象在定义任何事件侦听器时被垃圾收集?这是一种产生内存泄漏的来源吗?
    • 如果 IIFE 像这样:(function() { var temp = new EventEmitter(); temp.on("message", function(){console.log("event triggered.")});})();,在这种情况下,没有明显的异步代码来触发事件。 v8 是否会阻止局部变量 temp 被垃圾收集或在 IIFE 结束时将其从堆中删除?
    • @Bruce - 它知道child 仍然处于活动状态。当您注册侦听器时,它知道child 具有对您的回调函数的引用,该函数可以访问声明它的函数的内部。删除process.send() 不会改变任何内容。 V8 不知道回调是否会被调用。它只知道child 具有对回调的引用(在其侦听器数组中),因此“可以”调用它。我没有看到这会产生内存泄漏。当 child 完成并退出时,对回调函数的引用被释放并发生 gc。
    • @Bruce - 由于您可能是这里的新手,如果这回答了您的问题,您可以通过单击答案左侧的复选标记向社区表明这一点。这也将为您赢得一些声誉积分,因为您遵循正确的程序最终可以在这里为您赢得更多特权。
    猜你喜欢
    • 2011-04-11
    • 2013-04-06
    • 1970-01-01
    • 2011-02-19
    • 1970-01-01
    • 2011-04-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多