8

I'm aware of the great posts on Closures here and here, but neither seems to address the particular case I have in mind. The question is best demonstrated with code:

function foo() {
    var x = {};
    var y = "whatever";

    return function bar() {
        alert(y);
    };
}

var z = foo();

Referencing y within bar invokes a closure, and so long as I keep z around the garbage collector won't clean up y. The question is -- what happens to x? Is it held by that closure too even though it doesn't get referenced? Will the garbage collector see there's no reference x and clean it up? Or will x persists along with y as long as I hold onto z? (An ideal answer would cite the ECMA Specification.)

Community
  • 1
  • 1
user3786275
  • 103
  • 1
  • 7
  • See also [About closure, LexicalEnvironment and GC](https://stackoverflow.com/questions/8665781/about-closure-lexicalenvironment-and-gc) – Bergi May 25 '20 at 10:02

1 Answers1

11

The question is -- what happens to x?

The answer varies depending on theory vs. implementation.

In theory, yes, x is kept alive, because the closure (the anonymous function) has a reference to the binding object of the context of the call to foo, which includes x.

In practice, modern JavaScript engines are quite smart. If they can prove to themselves that x cannot be referenced from the closure, they can leave it out. The degree to which they do that will vary from engine to engine. Example: V8 (the engine in Chrome and elsewhere) will start out with x, y, and even the object that x refers to on the stack, not the heap; then when exiting foo, it looks to see what things still have outstanding references, and moves those to the heap. Then it pops the stack pointer, and the other things don't exist anymore. :-)

So, how can they prove it? Basically, if the code in the closure doesn't refer to it and doesn't use eval or new Function, the JavaScript engine is likely to be able to know that x isn't needed.


If you need to be sure that even if x still exists, the object is available for GC even on older browsers that might be literal (dumb) about it, you can do this:

x = undefined;

That means nothing keeps a reference to the object x used to refer to. So even though x still exists, at least the object it referred to is ready for reaping. And it's harmless. But again, modern engines will optimize things for you, I wouldn't worry about it unless you were faced with a specific performance problem and tracked it down to some code allocating large objects that aren't referenced once the function returns, but don't seem to be getting cleaned up.


Unfortunately, as you pointed out below, there are limits to this, such as the one mentioned in this question. But it's not all doom and gloom, see below under the profile snapshot for what you can do...

Let's look this code in V8, using Chrome's heap snapshot feature:

function UsedFlagClass_NoFunction() {}
function UnusedFlagClass_NoFunction() {}
function build_NoFunction() {
  var notused = new UnusedFlagClass_NoFunction();
  var used = new UsedFlagClass_NoFunction();
  return function() { return used; };
}

function UsedFlagClass_FuncDecl() {}
function UnusedFlagClass_FuncDecl() {}
function build_FuncDecl() {
  var notused = new UnusedFlagClass_FuncDecl();
  var used = new UsedFlagClass_FuncDecl();
  function unreachable() { notused; }
  return function() { return used; };
}

function UsedFlagClass_FuncExpr() {}
function UnusedFlagClass_FuncExpr() {}
function build_FuncExpr() {
  var notused = new UnusedFlagClass_FuncExpr();
  var used = new UsedFlagClass_FuncExpr();
  var unreachable = function() { notused; };
  return function() { return used; };
}

window.noFunction = build_NoFunction();
window.funcDecl = build_FuncDecl();
window.funcExpr = build_FuncExpr();

And here's the expanded heap snapshot:

no description available

When processing the build_NoFunction function, V8 successfully identifies that the object referenced from notused cannot be reached and gets rid of it, but it doesn't do so in either of the other scenarios, despite the fact that unreachable cannot be reached, and therefore notused cannot be reached through it.

So what can we do to avoid this kind of unnecessary memory consumption?

Well, for anything that can be handled via static analysis, we can throw a JavaScript-to-JavaScript compiler at it, like Google's Closure Compiler. Even in "simple" mode, the beautified result of "compiling" the code above with Closure Compiler looks like this:

function UsedFlagClass_NoFunction() {}
function UnusedFlagClass_NoFunction() {}
function build_NoFunction() {
    new UnusedFlagClass_NoFunction;
    var a = new UsedFlagClass_NoFunction;
    return function () {
        return a
    }
}

function UsedFlagClass_FuncDecl() {}
function UnusedFlagClass_FuncDecl() {}
function build_FuncDecl() {
    new UnusedFlagClass_FuncDecl;
    var a = new UsedFlagClass_FuncDecl;
    return function () {
        return a
    }
}

function UsedFlagClass_FuncExpr() {}
function UnusedFlagClass_FuncExpr() {}
function build_FuncExpr() {
    new UnusedFlagClass_FuncExpr;
    var a = new UsedFlagClass_FuncExpr;
    return function () {
        return a
    }
}
window.noFunction = build_NoFunction();
window.funcDecl = build_FuncDecl();
window.funcExpr = build_FuncExpr();

As you can see, static analysis told CC that unreachable was dead code, and so it removed it entirely.

But of course, you probably used unreachable for something during the course of the function, and just don't need it after the function completes. It's not dead code, but it is code you don't need when the function ends. In that case, you have to resort to:

unused = undefined;

at the end. Since you don't need the function anymore, you might also release it:

unused = unreachable = undefined;

(Yes, you can do that, even when it was created with a function declaration.)

And no, sadly, just doing:

unreachable = undefined;

...doesn't succeed (as of this writing) in making V8 figure out that unused can be cleaned up. :-(

Community
  • 1
  • 1
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • So the spec leaves this bit ambiguous? Know of any references indicating which browsers do what? – user3786275 Jun 28 '14 at 16:23
  • 1
    @user3786275: The spec is unambiguous. But implementations are free to optimize, provided the optimized code continues to behave as per the spec. Since there's no way to tell, just looking at the system from a spec viewpoint, whether `x` has been kept or not, an implementation removing it is fully compliant with the spec. (You'd have to have a memory profiler or similar to tell, which is out of scope for the spec). – T.J. Crowder Jun 28 '14 at 16:28
  • @user3786275: In don't know which engines do this other than that V8 does. But SpiderMonkey (the engine in Firefox and a couple of other places) is *very* smart these days. JScript improved *markedly* in IE9, then further in IE10, and further again in IE11; I can't say for certain, but I bet somewhere in there they started doing this as well. In terms of browser->engine mappings: Chrome, Chromium, and Opera all use V8; Firefox uses SpiderMonkey, and IE uses JScript. I think Safari uses JSC; I have no idea how advanced JSC is. IE8 is dumb and slow, so probably doesn't do this. :-) – T.J. Crowder Jun 28 '14 at 16:31
  • @user3786275: This concept of allocating objects on the stack and then copying them to the heap if they survive a function call has pretty deep roots in compiler circles. HotSpot (Oracle/Sun's JVM) has done it for years, for instance, to minimize Java garbage collection issues. So in the world of people writing compilers, this is no longer revolutionary. – T.J. Crowder Jun 28 '14 at 16:33
  • 1
    Your example definitely makes sense, and is roughly how I thought it worked. But this seems to suggest Chrome doesn't do such a good cleanup: http://stackoverflow.com/questions/19798803/how-javascript-closures-are-garbage-collected The bug is still present and the responses seem to be that though it'd be nice to do better, modern browsers don't: https://code.google.com/p/chromium/issues/detail?id=315190 But your explanation is detailed enough that it seems maybe this bug is merely old? – user3786275 Jun 28 '14 at 16:42
  • @user3786275: I'm running out the door, but briefly: I'm a bit surprised by that case not working and I'll have to prove it to myself (the linked question and issue are persuasive, though), but a V8 dev told me directly that the simpler case does work. FWIW. (I don't see why going further would be technically any big step. Huh.). Very interesting! – T.J. Crowder Jun 28 '14 at 16:49
  • @user3786275: Yup, that still happens. Not ideal, and I'm a bit surprised that it should be much of a leap to do. (See updated answer.) – T.J. Crowder Jun 28 '14 at 17:54
  • Any idea why the GC doesn't cleanup `unreachable` on one pass and then cleanup `notused` on a subsequent pass? It seems `unreachable` in the latter two cases is just as unreferenced as `notused` is in the original case. – user3786275 Jun 30 '14 at 15:13
  • @user3786275: It seems that way to me as well (not necessarily the multiple passes stuff, but that as `unreachable` can be demonstrated to be unreachable just like `notused` is, that it should be possible to optimize them both out), but apparently the extra level of indirection kills it (for V8, the engine Chromium/Chrome uses). For all we know, other engines (SpiderMonkey, the most recent JScript) do it. – T.J. Crowder Jun 30 '14 at 16:18
  • @user3786275: It's all about tradeoffs -- you don't want V8 to spend a lot of time hyper-optimizing your code, causing delays that users see (since JavaScript in web pages is always just-in-time compiled). And indeed, V8 is a two-phase optimizing compiler, so it could be that the second phase would detect this condition but we're just not triggering it with our test code. Or not. :-) I will note that running the test code through the Closure Compiler (a JavaScript-to-JavaScript compiler from Google) happily removes `unreachable`, bypassing this problem. – T.J. Crowder Jun 30 '14 at 16:22
  • Besides `eval` and `new Function`, I imagine `with` disables optimization as well? – kangax Jul 05 '14 at 16:35
  • @kangax: I should think so, yeah. :-) Using `with` means all unbound identifiers within it have to be resolved at runtime, every time (as you know). Although I can imagine limited scenarios where static analysis would tell you what they were (`var obj = {foo: 42}; with (obj) { console.log(foo); }`), I can't imagine V8/SpiderMonkey/etc. developers wasting precious time trying to detect those vs. the wide range where they *can't* know what's what until the expression is evaluated. I suspect (but don't know) that if they see `with`, they fall back to unoptimized mode. :-) – T.J. Crowder Jul 05 '14 at 18:08