TL;DR -- all closures created within a function are retained even if only one closure is ever referenced. This is really interesting, and potentially devastating for certain coding styles.
I ran the code on node and the RSS does appear to be increasing without bound. Even running node with --expose-gc and forcing a global.gc() inside logIt() causes the same unbounded memory growth.
Increasing the size of the array by a factor of 10 causes RSS usage to jump up by a factor of 10 every second, so we know that the memory usage isn't caused by creating a new long-lived closure (i.e., the logIt() function) every second.
In fact, removing the call to doSomethingWithStr() doesn't change the unbounded memory growth.
Here's a shorter snippet that demonstrates the leak more dramatically (about 10 MB/sec RSS growth):
var run = function () {
var str = new Array(10000000).join('*');
var fn = function() {
str;
}
var fn = function() { }
setInterval(fn, 100);
};
setInterval(run, 1000);
To be clear: when you say "removing the call to doSomethingWithStr() doesn't change the unbounded memory growth" you do literally mean "removing the call" and not "removing the function definition", right? That matches what I see.
>> TL;DR -- all closures created within a function are retained even if only one closure is ever referenced. This is really interesting, and potentially devastating for certain coding styles. <<
This is not devastating, it is how closures and scope chain work in JavaScript. See my detailed explanation below.
The crux of the matter is that the closure still references the outer function's scope chain, even after the outer function has returned. And the outer functions's scope chain is referenced by the closure until the closure is destroyed or returns.
I'm a very lazy (oops) language implementer. Every time but one when I've implemented closures, I took the quick approach of just copying the entire frame to the heap.
The other time, I was able to do more data flow analysis, but it also resulted in a bunch of annoying fiddly bugs and took more maintenance.
I'm not suggesting the v8 team took the 'easy way' out, but doing the deep introspection is hard, and in an environment such as a browser, I can see trading a pathological case such as this for what must be 1,000,000,000 inappropriate aggressive gc bugs.
Note that the TL;DR may well be V8-specific; other engines optimize closures differently and may not have the same action-at-a-distance interaction between closures.
But the real killer is that you can't tell which closures will keep what alive unless you assume that every closure keeps everything it closes over (even if it doesn't use it) alive. Nothing in the ES spec requires them not to...
Is it that, only one closure is created for variables defined in the function run, and variables needed in any inner-function are added to run's closed context? That seems like a bug fixable without impacting the language.
I ran the code on node and the RSS does appear to be increasing without bound. Even running node with --expose-gc and forcing a global.gc() inside logIt() causes the same unbounded memory growth.
Increasing the size of the array by a factor of 10 causes RSS usage to jump up by a factor of 10 every second, so we know that the memory usage isn't caused by creating a new long-lived closure (i.e., the logIt() function) every second.
In fact, removing the call to doSomethingWithStr() doesn't change the unbounded memory growth.
Here's a shorter snippet that demonstrates the leak more dramatically (about 10 MB/sec RSS growth):
Tried it out on node v0.8.18