Normally, Javascript variables defined in a function with keyword 'var' should be function-scoped. But I am confused about a situation related with node.js child process. I have two files, app.js (which is the main file) and child.js (which is the execution script for the child process):
// 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, computationally intensive task, takes tens of seconds to get finished
function fib(n) {
return n < 2 ? 1 : fib(n - 1) + fib(n - 2);
}
var result = fib(46);
process.send(JSON.stringify(result));
I was expecting this piece of code may throw some exceptions because the variable 'child' is a local object created in function bar(). It's supposed to be garbage-collected as the function bar() finishes execution. The event handler registered for receiving child process messages is just kept in the hash table of 'child' object (child object is also an instance of EventEmitter based on docs of node.js child process module), so those hash tables should also be garbage collected as the local 'child' object being reclaimed.
But the real situation is that tens of seconds later, when the child process finishes calculating fib(46) and sending back the result through process.send, the parent process successfully emits the event and finally gets the registered event handler triggered:
$ node --expose-gc .\app.js
fib(46) = 2971215073
So, it looks like the child process object (that object pointing by the local variable 'child') is still in the memory? Or that child process object is referenced internally by node.js child process module, so it can not be garbage collected before the end of the child process? I really need somebody to help me clarify the situation.
Garbage collection can collect something and recycle its memory ONLY when there is no other code that can reach or use or reference that variable. Sometimes that situation occurs when a function declares a local variable and then the function executes and returns, but not always.
If there are asynchronous operations running (and your child process is an asynchronous operation) and event handlers or callbacks declared within that function scope and those callbacks/events could still be called, then the reachable variables in that function scope cannot be garbage collected until they are truly not reachable any more which, in your case, may be long, long after the function has already executed and returned because that lifetime is associated with the lifetime of the child process, not with the lifetime of the function execution.
In your specific case, because the child
object can still emit events long after the function has returned, then then that child
object and anything else that its event handlers may access cannot be garbage collected until the child object itself is GCed or until some other situation tells the garbage collector that that event handler can't be called any more.
So, keep in mind that in Javascript, local variables in a function do not go into a strict stack frame that is 100% recycled as soon as the function returns. Instead, they are properties on a scope object and each individual property on that scope object can only be garbage collected when it is no longer reachable by any active code. So, it's possible for some things in a scope object to get garbage collected because they aren't referenced in any code that can still be called while other things on the scope object cannot yet be garbage collected because there are callbacks declared within that function scope that may still reference some of those variables declared within that function. The same rules apply to either a function-scoped or a block-scoped variable as a function scope is just a larger block than a block scope - they now work pretty much the same.
Here's a fairly simple example:
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();
In this code, the local variable msg
is available for GC as soon as the function run()
exits.
The variable total
is not available for GC until after the clearInterval()
is called and that final timer callback finishes.