1

I am trying to understand how the garbage collection in Node.js works.

A big array is created and stored globally. When tracking the Heap Usage, I saw that once the array is created, the Heap Usage goes up drastically. No wonder, since the array is huge. But it seems like the garbage collector does not remove the array - no matter how long I wait, the Heap Usage stays the same. Does the garabge collector think, the array is still needed?

let bigArray
setTimeout(function() {
    bigArray = new Array(9999999)
}, 5000)

setInterval(function () {
    const used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(`Usage: ${Math.round(used * 100) / 100} MB`);
}, 1000)
```
Louis
  • 13
  • 4

2 Answers2

0

The garbage collector does not collect all the time. Only when your computer is running out of memory.

Try this post and trigger the garbage collector by hand. Will it still have the memory leak?

How to request the Garbage Collector in node.js to run?

Also the array is still referenced, if you set it to null somewhere it might get collected.

Niels
  • 48,601
  • 4
  • 62
  • 81
0

The array is still referenced in the module functions closure so can't be garbage collected:

// Implicit closure created by nodejs
(function(exports, require, module, __filename, __dirname) {
   let bigArray
   setTimeout(function() {
      bigArray = new Array(9999999)
   }, 5000);

   setInterval(function () {
      // `bigArray` is still in scope here so cannot be garbage collected
      // If you set bigArray to undefined it will get GC'd
      const used = process.memoryUsage().heapUsed / 1024 / 1024;
      console.log(`Usage: ${Math.round(used * 100) / 100} MB`);
   }, 1000);
});

If you were to declare the variable only inside the setTimeout function scope it would be GC'd without having to remove the reference yourself, e.g.

// Implicit closure created by nodejs
(function(exports, require, module, __filename, __dirname) {
   setTimeout(function() {
      let bigArray = new Array(9999999)
   }, 5000);

   setInterval(function () {
      // `bigArray` is not in scope here so it will be GC'd
      const used = process.memoryUsage().heapUsed / 1024 / 1024;
      console.log(`Usage: ${Math.round(used * 100) / 100} MB`);
   }, 1000);
});

Additionally, be aware that V8 could be behaving lazily when it's not constrained for memory.

You can try running your script with --expose-gc and force gc:

node --expose-gc index.js
const forceGC = () => {
    if (global.gc) {
        global.gc();
    } else {
        console.warn('No GC hook! Start your program as `node --expose-gc file.js`.');
    }
}

let bigArray
setTimeout(function() {
    bigArray = new Array(9999999)
}, 5000)

setInterval(function () {
    bigArray = undefined;
    forceGC();
    const used = process.memoryUsage().heapUsed / 1024 / 1024;
    console.log(`Usage: ${Math.round(used * 100) / 100} MB`);
}, 1000)

Also, you can give node less memory to work with to test how it behaves when it's running low on memory:

node --max-old-space-size=100 index.js // Give node just 100MB (defaults to 2GB)
Richard Scarrott
  • 6,638
  • 1
  • 35
  • 46
  • This is interesting, thank you very much! :) Tested it multiple times, and it seems that the GB does not remove the big Array. I read somewhere else, that references from within setTimeout, setInterval etc. cause the GB to think, that the entity is still needed. – Louis Feb 06 '21 at 13:33
  • Yeh, I re-read your question and realised you were seeing the memory stay the same (I had initially thought your first setTimeout was a setInterval and you were seeing memory continuously increase). I've updated my answer to try to explain what you're seeing. – Richard Scarrott Feb 06 '21 at 13:41