ASDFGerte ASDFGerte - 3 months ago 23
Javascript Question

Closure memory leak of unused variables

I'd like to understand under which circumstances variables which are no further used are stored in closures and lead to memory leaks. My most preferred outcome would be "there are none", but this doesn't seem to be the case.

From what I understand, once a function is declared inside another function, its internal [[scope]] is assigned the LexicalEnvironment of its encapsulating function. This LexicalEnvironment has reference local variables and the entire scope chain at that point. This basically includes all free variables the function could access (from what I understood of lostechies, javascript closures explained).

Here the first issue arises: this should mean all those variables can be reached as long as the function lives. E.g. the following should already leak:



function a() {
let big = new Array(1000000).join('*'); //never accessed
//function unused() { big; }
return () => void 0;
}

let fstore = [];
function doesThisLeak() {
for(let i = 0; i < 100; i++) fstore.push(a());
}

doesThisLeak();





This luckily doesn't seem to be the case on my firefox. I've received several explanations to why this doesn't leak, from "the jitter is smart" to "LexicalEnvironment is a record type which means GC can collect the unused variables". I still don't know whether either is correct, whether this doesn't leak on all modern runtimes and why.

After further research, I found auth0, four types of leaks in javascript (sadly, there appears to be no html id to jump to, the relevant part is "4: Closures") which shows a way to trick whatever smart thing is collecting the unused variables. In above snippet, when just uncommenting the "unused" function, I do not see RAM usage ever going down again (it was already noted that it could be GC simply did not run for other reasons. However, so far, I am assuming it leaks. I also got told this was limited to firefox, but it appeared to produce similar behavior in chrome)

This example (in case it really does what i believe it does), shows that completely unused variables can leak due to a function declaration in the same scope.

To conclude my problems:


  1. What is the reason for, in the above snippet, "big" getting collected (when "unused" is commented) and does this happen on all modern runtimes?

  2. Assuming the example with the "unused" function not commented leaks, what are best practices to avoid such accidental leaks? Are there any more? I already got the suggestion of null'ing all local variables which are not further used at the end of functions, however, this seems absurdly ugly. I fear using temporary variables for pre-calculations and accidentally leaking.



PS: It is quite hard to make certain that this question has not already been asked in the jungle of questions about memory leaks with closures.

Answer

The compiler can examine the code of the returned function to see which free variables it references, and only those variables need to be saved in the closure, not the entire LexicalEnvironment. You can see this by examining the closure in the Javascript debugger.

function a() {
  let big = new Array(1000000).join('*');
  let small = "abc"; // is accessed
  return (x) => small + x;
}

fun = a();
console.dir(fun);

function b() {
    let big = "pretend this is a really long string";
    function unused() { big; }
    return () => void 0;
}

fun = b();
console.dir(fun);

When you expand the first function in the debugger, you'll see small in the Closure property, but not big. Unfortunately, the Chrome compiler doesn't seem to be clever enough to detect when the variable is referenced in an unused function that isn't returned, so it doesn't need to be saved, so we get a leak in b().

enter image description here

Any data that isn't saved in the closure becomes garbage and can be collected, so it won't leak.

Comments