Closure memory leak of unused variables
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:
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.
big
return () => big;
big
fstore
sorry what's GC again?
– Martian2049
Sep 6 '17 at 2:20
3 Answers
3
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()
.
small
Closure
big
b()
Any data that isn't saved in the closure becomes garbage and can be collected, so it won't leak.
Thanks, this solves a large portion of the problem. Issues that remain: "The compiler can examine", is this not by spec, can I depend on this? Also are there more pitfalls like the described one, where a reference in an unused function tricks the compiler into thinking it is needed?
– ASDFGerte
Aug 8 '16 at 21:56
It's a compiler optimization, so I don't think you can depend on it. As you've seen, it's not perfect.
– Barmar
Aug 8 '16 at 21:59
As for best practices on code, any suggestions? I guess hoping for this compiler optimization to occur in all modern browsers and explicitly nulling memory-intensive non-exposed local variables when it gets more complicated is a start. Are there more known situations where this optimization fails? I'd prefer not to always have to null all variables at the end of functions.
– ASDFGerte
Aug 8 '16 at 22:20
Unless the closure is very long-lived, like an event handler, I wouldn't worry too much about it. If it's just a one-time callback, it will presumably be called pretty soon, and then discarded, so the entire closure will become garbage. And you don't have to null all variables, just the ones that are likely to consume huge amounts of memory, the rest are probably not going to cause much impact.
– Barmar
Aug 9 '16 at 1:20
And don't forget: premature optimization is the root of all evil. Wait until you actually have a memory problem before worrying about this.
– Barmar
Aug 9 '16 at 1:28
After you call a()
your fstore
has reference to created function () => void
only. After a()
returns, its scope vars will be deleted. It means that no vars has reference to your new Array(1000000).join('*')
and it will be garbage collected. It will be collected the same way if you uncomment unused
function because it will deleted too. There are no leaks in your code.
a()
fstore
() => void
a()
new Array(1000000).join('*')
unused
Using chrome's debugger like Barmar said, you can see that when "unused" is uncommented, "big" is being closed over (at least in my chrome). SS of debugger
– ASDFGerte
Aug 8 '16 at 21:46
@ASDFGerte my fail, Barmar explained this. But i think its browsers fail, it's should not be stored in closure. If you breakpoint your
() => void 0
function execution you cant access big
in console, it says undefined
for me.– Maxx
Aug 8 '16 at 22:54
() => void 0
big
undefined
pic1
you can think about Javascript scope chain with the problem, Identify a Function will create a new scope chain , if function's local scope not have a local variable, it will reference outside lexical environment scope, it will keep over in the memory
here's a link
Thanks for contributing an answer to Stack Overflow!
But avoid …
To learn more, see our tips on writing great answers.
Required, but never shown
Required, but never shown
By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.
Modern engines will keep only those variables from outer scopes actually used within the inner functions. In your case, since the returned inner function does not use
big
, no reference to it will be maintained. if instead your inner function wasreturn () => big;
, then yes,big
would be maintained, but it would have to be, right? Whatever memory was involved would be GC'd anyway oncefstore
went out of scope. You can't garbage collect something which isn't garbage, and not doing so is not a "leak".– user663031
Aug 8 '16 at 20:52