170

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?

Paul Draper
  • 78,542
  • 46
  • 206
  • 285
  • 4
    For the uninitiated, how does Chrome let you test which variables/objects are garbage collected, and when that happens? – nnnnnn Nov 05 '13 at 20:58
  • Chrome (webkit, really) lets you take a heap snapshot: https://developers.google.com/chrome-developer-tools/docs/heap-profiling. I will add that to my question. – Paul Draper Nov 05 '13 at 21:00
  • 1
    Maybe the console is keeping a reference to it. Does it get GCed when you clear the console? – david Nov 05 '13 at 21:01
  • 1
    @david In the last example the `unreachable` function is never executed so nothing is actually logged. – James Montagne Nov 05 '13 at 21:05
  • @david, you are correct. `console.log` *will* keep a reference if the Dev Tools are open when it is logged. I changed the examples so that can't happen anymore. It does not change the results. – Paul Draper Nov 05 '13 at 21:05
  • @PaulDraper What's happening if you call `f` multiple times? `f_ = f(); f1_ = f(); f2_ = f();` Do you see more than one instance in the object's count? – plalx Nov 05 '13 at 21:47
  • @plalx, yes, after running that, there are three `someClass` instances on the heap. – Paul Draper Nov 05 '13 at 21:51
  • 1
    I'm having trouble to believe that a bug of that importance went through, even if we seem to be faced with the facts. However I'm looking at the code again and again and I do not find any other rationnal explanation. You tried not to run the code in the console at all right (a.k.a let the browser run it naturally from a loaded script)? – plalx Nov 05 '13 at 21:59
  • @plalx, I had the same disbelief when a coworker told me the hacks he was trying to reduce memory usage, and I reduced the problem to this. Yes, I ran these "normally". I added a URL for the last example: s3.amazonaws.com/chromebugs/index.html – Paul Draper Nov 05 '13 at 22:20
  • Have you tried to do `some=null` when you don't want to access it any more (inside the timeout?) – some Nov 05 '13 at 22:25
  • I tried your example, and after I do `some=null` inside the timeout, the object is no longer found in the snapshot. – some Nov 05 '13 at 22:31
  • May I suggest a document from IBM from 2007, about memory leaks in javascript: http://www.ibm.com/developerworks/web/library/wa-memleak/ – some Nov 05 '13 at 22:45
  • @some, yes, manually setting some to `null` will work. Another workaround is to not use a closure. I've tested both of these workaround. I do appreciate your suggestion. – Paul Draper Nov 05 '13 at 22:45
  • 1
    @some, I've read that article before. It is subtitled "Handling circular references in JavaScript applications", but the concern of JS/DOM circular references applies to no modern browser. It mentions closures, but in all of the examples, the variables in question were still in possible use by the program. – Paul Draper Nov 05 '13 at 22:48
  • 1
    @plalx: Google Chrome has been known to have memory leaks in the past - often detected only when they hit high profile sites like Apple or Amazon. So it's not totally unheard of. Still, it's very, very rare and is often very quickly fixed. Paul did the right thing in submitting a bug report. – slebetman Nov 06 '13 at 01:28

6 Answers6

78

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.

Jeremy
  • 1
  • 85
  • 340
  • 366
some
  • 48,070
  • 14
  • 77
  • 93
  • Killing `window.f_` would be the better choice, or else you still be leaking the closure itself... – nmaier Nov 05 '13 at 23:10
  • 4
    But, once the `setTimeout()` callback runs, that function scope of the `setTimeout()` callback is done and that whole scope should be garbage collected, releasing its reference to `some`. There is no longer any code that can run that can reach the instance of `some` in the closure. It should be garbage collected. The last example is even worse because `unreachable()` is not even called and nobody has a reference to it. Its scope should be GCed also. These both seem like bugs. There is no language requirement in JS to "free" things in a function scope. – jfriend00 Nov 05 '13 at 23:17
  • I'm not sure that quote necessarily means setting `some` to `null`. I don't have to set `some` to `null` in the first case. Alternative explanation: by not including `some` in the surviving closure `window.f_`, I explicitly made it unreachable. If I had unwisely used the closure `return function() { if(false) some; }`, `some` would be in practice unreachable, but not explicitly unreachable, and thus subject to the described limitation. But, as the Mozilla document says, that occurrence is *rare*. By contrast, using two closures which use different variables in *not* rare IMHO. – Paul Draper Nov 05 '13 at 23:33
  • @jfriend00 But it is still in the closure of the empty function that is returned in `f()`. – some Nov 05 '13 at 23:33
  • 1
    @some It shouldn't. Function's aren't supposed to close over variables they aren't using internally. – plalx Nov 05 '13 at 23:35
  • 2
    It could be accessed by the empty function, but it isn't so there are no actual references to it so it should be clear. Garbage collection keeps track of actual references. It's not supposed to hold onto everything that could have been referenced, only the things that are actually referenced. Once the last `f()` is called, there are no actual references to `some` any more. It is unreachable and should be GCed. – jfriend00 Nov 05 '13 at 23:46
  • 1
    @jfriend00 I can't find anything in the (standard)[http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf] says anything about only the variables that it uses internally should be available. In section 13, the production step 4: *Let closure be the result of creating a new Function object as specified in 13.2*, 10.2 "The outer environment reference is used to model the logical nesting of Lexical Environment values. The outer reference of a (inner) Lexical Environment is a reference to the Lexical Environment that logically surrounds the inner Lexical Environment." – some Nov 06 '13 at 00:01
  • So If I understand that correctly (English isn't my native language) everything that is declared in the outer scope becomes part of the closure. I can't find anything about that it filters what should be available to the inner functions. – some Nov 06 '13 at 00:01
  • @jfriend00 is absolutely correct. In fact, in the Chrome Dev Tools, you can put a breakpoint in that closure, and call `window.f_`. When it breaks, try to reference `some`. You can't. If `some` *had* been referenced in the closure definition, then you could reference in there while debugging. But for all the debugger knows, it's been GC'ed. Unfortunately, the GC does not seem to be as intelligent as the debugger gives it credit for. – Paul Draper Nov 06 '13 at 00:01
  • @PaulDraper I tried that with `function a(b) { console.log(b); } a(f_);` and the breakpoint at `console.log` and there it is in the closure. – some Nov 06 '13 at 00:03
  • And @some, the language standard is more about the language. I doubt you will find much about GC. Really, you could do no GC and still fulfill all language requirements about expected functionality. – Paul Draper Nov 06 '13 at 00:04
  • @some - GC is not only by entire scopes at a time. It's supposed to be individual references to individual variables/objects. This is particularly important for objects and arrays, things that are passed by reference and can be in use by many different scopes. – jfriend00 Nov 06 '13 at 00:04
  • @some, perhaps I was unclear. Try `function f() { var some = 1; return function() { return 0; }; } f();`, break at `return 0;`, and try to reference `some`. – Paul Draper Nov 06 '13 at 00:06
  • @PaulDraper Try `function f() {var some = new someClass();function unreachable() {some;} return function(){return 0;};} window.f_ = f();` and set the breakpoint at `return 0` (had to add a statement to set the breakpont on) and you find it in the closure. – some Nov 06 '13 at 00:20
  • @some, you're right, it looks like the GC and the debugger have the same behavior. Along the lines of my original concern though, if retry that without `function unreachable() { some; }`, `some` is no longer reachable from `return 0;` (because it was smart enough to GC in that case). – Paul Draper Nov 06 '13 at 00:25
  • @PaulDraper I have now tried it Chrome 30 and FF25 with same results. I also tried Opera 12.16 and IE10. Chrome and FF are smart enough to remove `some` in some cases. In Opera and IE, `some` is available to the returning function even if it isn't used. So as far as I can see, this is not a bug. The memory is released if the reference in `window.f_` is cleared. – some Nov 06 '13 at 00:48
  • 2
    Well, `eval` is really special case. For example, `eval` cannot be aliased (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval), e.g. `var eval2 = eval`. If `eval` is used (and since it cannot be called by a different name, that is easy to do), then we must assume it can use anything in scope. – Paul Draper Nov 06 '13 at 02:52
  • 1
    @PaulDraper I think `eval` *can* be aliased, but the behavior of an aliased `eval` is to evaluate the code in the *global scope*, rather than the scope of the function that made the `eval` call (so, the net result is the same: no need to save variables from GC, because an aliased `eval` runs globally and can't use closure-scope vars). I think the "possible runtime error" that MDN references here arises because `myEval("x+y")` run in global scope, where there might not be any `x` or `y`. So, MDN should probably say "an aliased `eval` can't access local-scope variables" (assuming I'm right). – apsillers Nov 06 '13 at 16:18
  • 1
    Downvoting because this answer is missing the point. Basically, the spec says what the program *should* do, not what it should not; nor does it proscribe a particular implementation. A function needs to be able access its lexical scope, but that doesn't mean there ever needs to be a "lexical scope" object (though there may well be), nor that that scope needs to actually persistently store *anything* - it just needs to be *able* to access it; but if it doesn't, why store anything? Also, it doesn't say inaccessible things must be freed. Better: http://stackoverflow.com/a/19803948/42921 – Eamon Nerbonne Nov 13 '13 at 00:24
  • 1
    @EamonNerbonne Thank you for describing why you downvoted. I agree that my answer isn't answering the question in the headline "How JavaScript closures are garbage collected" and that *Boris Zbarsky*s answer tries to answer that, but since a bug report was filed I try to determinate if this actually is a bug or not. I tested it in four different browsers and showed the result. I'm sorry that you don't find that useful. – some Nov 13 '13 at 01:54
  • This is a bit off-topic from the original question, but after further consideration, I believe "avoidable and extremely poor (though apparently intentional) behavior" is perhaps more accurate than "bug". It's like the issue C++ compilers had for many years, where they could parse `vector >` but not `vector>`. All compilers had this issue, but just because they all did the same thing, didn't mean it was fine. And eventually, it was fixed. – Paul Draper Nov 13 '13 at 02:45
  • @PaulDraper I'm happy that we can agree about something :) By the way, do you know why this suddenly has got so much attention? It's a week old but today it got a lot of votes, and got 3 edits from other people. – some Nov 13 '13 at 05:12
  • 1
    @some, I wondered the same thing. Then I found it: https://news.ycombinator.com/item?id=6721613 – Paul Draper Nov 13 '13 at 05:14
  • 1
    Was @some attracted to answering this question because of the class name used in the examples? Seems like a good case of nominative determinism to me. :) – Giles Roberts Nov 18 '13 at 15:43
  • 1
    @GilesRoberts It was just a happy coincidence, but I am happy that you noticed it. :) – some Nov 18 '13 at 16:08
50

I tested this in IE9+ and Firefox.

function f() {
  var some = [];
  while(some.length < 1e6) {
    some.push(some.length);
  }
  function g() { some; } //removing this fixes a massive memory leak
  return function() {};   //or removing this
}

var a = [];
var interval = setInterval(function() {
  var len = a.push(f());
  if(len >= 500) {
    clearInterval(interval);
  }
}, 10);

Live site here.

I hoped to wind up with an array of 500 function() {}'s, using minimal memory.

Unfortunately, that was not the case. Each empty function holds on to an (forever unreachable, but not GC'ed) array of a million numbers.

Chrome eventually halts and dies, Firefox finishes the whole thing after using nearly 4GB of RAM, and IE grows asymptotically slower until it shows "Out of memory".

Removing either one of the commented lines fixes everything.

It seems that all three of these browsers (Chrome, Firefox, and IE) keep an environment record per context, not per closure. Boris hypothesizes the reason behind this decision is performance, and that seems likely, though I'm not sure how performant it can be called in light of the above experiment.

If a need a closure referencing some (granted I didn't use it here, but imagine I did), if instead of

function g() { some; }

I use

var g = (function(some) { return function() { some; }; )(some);

it will fix the memory problems by moving the closure to a different context than my other function.

This will make my life much more tedious.

P.S. Out of curiousity, I tried this in Java (using its ability to define classes inside of functions). GC works as I had originally hoped for Javascript.

Paul Draper
  • 78,542
  • 46
  • 206
  • 285
16

Heuristics vary, but a common way to implement this sort of thing is to create an environment record for each call to f() in your case, and only store the locals of f that are actually closed over (by some closure) in that environment record. Then any closure created in the call to f keeps alive the environment record. I believe this is how Firefox implements closures, at least.

This has the benefits of fast access to closed-over variables and simplicity of implementation. It has the drawback of the observed effect, where a short-lived closure closing over some variable causes it to be kept alive by long-lived closures.

One could try creating multiple environment records for different closures, depending on what they actually close over, but that can get very complicated very quickly and can cause performance and memory problems of its own...

Boris Zbarsky
  • 34,758
  • 5
  • 52
  • 55
  • 1
    thank you for your insight. I have come to conclude this is also how Chrome implements closures as well. I always thought they were implemented in the latter way, in which each closure kept only the environment that it needed, but such is not the case. I wonder whether it is really that complicated to create multiple environment records. Rather than aggregate the closures' references, act as if each one were the only closure. I had guessed that performance considerations were the reasoning here, though to me the consequences of having a shared environment record seem even worse. – Paul Draper Nov 06 '13 at 07:45
  • The latter way in some cases leads to an explosion in the number of environment records that need to be created. Unless you try hard to share them across functions when you can, but then you need a bunch of complicated machinery to do that. It's possible, but I'm told the performance tradeoffs favor the current approach. – Boris Zbarsky Nov 06 '13 at 16:43
  • The number of records is equal to the the number of created closures. I might described `O(n^2)` or `O(2^n)` as an explosion, but not a proportional increase. – Paul Draper Nov 06 '13 at 20:38
  • Well, O(N) is an explosion compared to O(1), especially when each one can take up a fair amount of memory... Again, I'm not an expert on this; asking on the #jsapi channel on irc.mozilla.org is likely to get you a better and more detailed explanation than I can provide of what the tradeoffs are. – Boris Zbarsky Nov 07 '13 at 03:47
  • It's also good to note that there is no practical problem since triggering this requires deliberate setup with some pretty unplausibly weird code. – Esailija Nov 13 '13 at 19:02
  • @Esailija Triggering which this? – Boris Zbarsky Nov 14 '13 at 01:05
  • @BorisZbarsky triggering whatever the OP perceives is a problem :) – Esailija Nov 14 '13 at 08:58
  • 2
    @Esailija It's actually pretty common, unfortunately. All you need is a large temporary in the function (typically a large typed array) that some random short-lived callback use and a long-lived closure. It's come up a number of times recently for people writing web apps... – Boris Zbarsky Nov 14 '13 at 14:00
0
  1. Maintain State between function calls Let’s say you have function add() and you would like it to add all the values passed to it in several calls and return the sum.

like add(5); // returns 5

add(20); // returns 25 (5+20)

add(3); // returns 28 (25 + 3)

two way you can do this first is normal define a global variable Of course, you can use a global variable in order to hold the total. But keep in mind that this dude will eat you alive if you (ab)use globals.

now latest way using closure with out define global variable

(function(){

  var addFn = function addFn(){

    var total = 0;
    return function(val){
      total += val;
      return total;
    }

  };

  var add = addFn();

  console.log(add(5));
  console.log(add(20));
  console.log(add(3));
  
}());
Avinash Maurya
  • 332
  • 3
  • 4
0

function Country(){
    console.log("makesure country call"); 
   return function State(){
   
    var totalstate = 0; 
 
 if(totalstate==0){ 
 
 console.log("makesure statecall"); 
 return function(val){
      totalstate += val;  
      console.log("hello:"+totalstate);
    return totalstate;
    } 
 }else{
  console.log("hey:"+totalstate);
 }
  
  };  
};

var CA=Country();
 
 var ST=CA();
 ST(5); //we have add 5 state
 ST(6); //after few year we requare  have add new 6 state so total now 11
 ST(4);  // 15
 
 var CB=Country();
 var STB=CB();
 STB(5); //5
 STB(8); //13
 STB(3);  //16

 var CX=Country;
 var d=Country();
 console.log(CX);  //store as copy of country in CA
 console.log(d);  //store as return in country function in d
Avinash Maurya
  • 332
  • 3
  • 4
0

(function(){

   function addFn(){

    var total = 0;
 
 if(total==0){ 
 return function(val){
      total += val;  
      console.log("hello:"+total);
    return total+9;
    } 
 }else{
  console.log("hey:"+total);
 }
  
  };

   var add = addFn();
   console.log(add);  
   

    var r= add(5);  //5
 console.log("r:"+r); //14 
 var r= add(20);  //25
 console.log("r:"+r); //34
 var r= add(10);  //35
 console.log("r:"+r);  //44
 
 
var addB = addFn();
  var r= addB(6);  //6
  var r= addB(4);  //10
   var r= addB(19);  //29
    
  
}());
Avinash Maurya
  • 332
  • 3
  • 4