How JavaScript closures are garbage collected

Paul Draper picture Paul Draper · Nov 5, 2013 · Viewed 20k times · Source

I've logged the following Chrome bug, which has led to many serious and non-obvious memory leaks in my code:

(These results use Chrome Dev Tools' memory profiler, which runs the GC, and then takes a heap snapshot of everything not garbaged collected.)

In the code below, the someClass instance is garbage collected (good):

var someClass = function() {};

function f() {
  var some = new someClass();
  return function() {};
}

window.f_ = f();

But it won't be garbage collected in this case (bad):

var someClass = function() {};

function f() {
  var some = new someClass();
  function unreachable() { some; }
  return function() {};
}

window.f_ = f();

And the corresponding screenshot:

screenshot of Chromebug

It seems that a closure (in this case, function() {}) keeps all objects "alive" if the object is referenced by any other closure in the same context, whether or not if that closure itself is even reachable.

My question is about garbage collection of closure in other browsers (IE 9+ and Firefox). I am quite familiar with webkit's tools, such as the JavaScript heap profiler, but I know little of other browsers' tools, so I haven't been able to test this.

In which of these three cases will IE9+ and Firefox garbage collect the someClass instance?

Answer

some picture some · Nov 6, 2013

As far as I can tell, this is not a bug but the expected behavior.

From Mozilla's Memory management page: "As of 2012, all modern browsers ship a mark-and-sweep garbage-collector." "Limitation: objects need to be made explicitly unreachable".

In your examples where it fails some is still reachable in the closure. I tried two ways to make it unreachable and both work. Either you set some=null when you don't need it anymore, or you set window.f_ = null; and it will be gone.

Update

I have tried it in Chrome 30, FF25, Opera 12 and IE10 on Windows.

The standard doesn't say anything about garbage collection, but gives some clues of what should happen.

  • Section 13 Function definition, step 4: "Let closure be the result of creating a new Function object as specified in 13.2"
  • Section 13.2 "a Lexical Environment specified by Scope" (scope = closure)
  • Section 10.2 Lexical Environments:

"The outer reference of a (inner) Lexical Environment is a reference to the Lexical Environment that logically surrounds the inner Lexical Environment.

An outer Lexical Environment may, of course, have its own outer Lexical Environment. A Lexical Environment may serve as the outer environment for multiple inner Lexical Environments. For example, if a Function Declaration contains two nested Function Declarations then the Lexical Environments of each of the nested functions will have as their outer Lexical Environment the Lexical Environment of the current execution of the surrounding function."

So, a function will have access to the environment of the parent.

So, some should be available in the closure of the returning function.

Then why isn't it always available?

It seems that Chrome and FF is smart enough to eliminate the variable in some cases, but in both Opera and IE the some variable is available in the closure (NB: to view this set a breakpoint on return null and check the debugger).

The GC could be improved to detect if some is used or not in the functions, but it will be complicated.

A bad example:

var someClass = function() {};

function f() {
  var some = new someClass();
  return function(code) {
    console.log(eval(code));
  };
}

window.f_ = f();
window.f_('some');

In example above the GC has no way of knowing if the variable is used or not (code tested and works in Chrome30, FF25, Opera 12 and IE10).

The memory is released if the reference to the object is broken by assigning another value to window.f_.

In my opinion this isn't a bug.