node.js 子进程对象生命周期

node.js child process object lifecycle

通常,Javascript 在函数中使用关键字 'var' 定义的变量应该是函数范围的。但是我对与 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' 对象的散列 table 中(根据 node.js 子进程模块的文档,子对象也是 EventEmitter 的实例),所以那些散列 tables 也应该作为被回收的本地 'child' 对象被垃圾收集。

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

$ 节点 --expose-gc .\app.js

fib(46) = 2971215073

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

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

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

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

因此,请记住,在 Javascript 中,函数中的局部变量不会进入严格的堆栈帧,该堆栈帧在函数 returns 后立即被 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。