Consider the following example:
function makeFunction() {
let x = 3;
let s = "giant string, 100 MB in size";
return () => { console.log(x); };
}
// Are both x and s held in memory here
// or only x, because only x was referred to by the closure returned
// from makeFunction?
let made = makeFunction();
// Suppose there are no further usages of makeFunction after this point
// Let's assume there's a thorough GC run here
// Is s from makeFunction still around here, even though made doesn't use it?
made();
So if I close around just one variable from a parent lexical environment, is that variable kept around or is every sibling variable in its lexical environment also kept around?
Also, what if makeFunction was itself nested inside another outer function, would that outer lexical environment be retained even though neither makeFunction nor makeFunction's return value referred to anything in that outer lexical environment?
I'm asking for performance reasons - do closures keep a bunch of stuff around or only what they directly refer to? This impacts memory usage and also resource usage (e.g. open connections, handles, etc.).
This would be mostly in a NodeJS context, but could also apply in the browser.
V8 developer here. This is a bit complicated ;-)
The short answer is: closures only keep around what they need.
So in your example, after makeFunction
has run, the string referred to by s
will be eligible for garbage collection. Due to how garbage collection works, it's impossible to predict when exactly it'll be freed; "at the next garbage collection cycle". Whether makeFunction
runs again doesn't matter; if it does run again, a new string will be allocated (assuming it was dynamically computed; if it's a literal in the source then it's cached). Whether made
has already run or will run again doesn't matter either; what matters is that you have a variable referring to it so you could run it (again). Engines generally can't predict which functions will or won't be executed in the future.
The longer answer is that there are some footnotes. For one thing, as comments already pointed out, if your closure uses eval
, then everything has to be kept around, because whatever source snippet is eval
'ed could refer to any variable. (What one comment mentioned about global variables that could be referring to eval
is not true though; there is a semantic difference for "global eval", a.k.a. "indirect eval": it cannot see local variables. Which is usually considered an advantage for both performance and debuggability -- but even better is to not use eval
at all.)
The other footnote is that somewhat unfortunately, the tracking is not as fine-grained as it could be: each closure will keep around what any closure needs. We have tried fixing this, but as it turns out finer-grained tracking causes more memory consumption (for metadata) and CPU consumption (for doing the work) and is therefore usually not worth it for real code (although it can have massive impact on artificial tests stressing precisely this scenario). To give an example:
function makeFunction() {
let x = 3;
let s = "giant string, 100 MB in size";
let short_lived = function() { console.log(s.length); }
// short_lived(); // Call this or don't, doesn't matter.
return function long_lived() { console.log(x); };
}
let long_lived = makeFunction();
With this modified example, even though long_lived
only uses x
, short_lived
does use s
(even if it's never called!), and there is only one bucket for "local variables from makeFunction
that are needed by some closure", so that bucket keeps both x
and s
alive. But as I said earlier: real code rarely runs into this issue, so this is usually not something you have to worry about.
Side note:
and also resource usage (e.g. open connections, handles, etc.)
As a very general statement (i.e., in any language or runtime environment, regardless of closures or whatnot), it's usually advisable not to rely on garbage collection for resource management. I recommend to free your resources manually and explicitly as soon as it is appropriate to free them.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With