Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
The Javascript memory leak we all do (atkinson.posterous.com)
92 points by ra on May 20, 2011 | hide | past | favorite | 29 comments


The OP is confused --- a proper garbage collector will collect cyclic structures.

The situation that he describes was a problem in versions of IE prior to ie8 because of limitations on its garbage collector --- specifically that it was unable to collect cycles if the cycle involved a DOM element. This is fixed in ie8 (and, presumably, subsequent versions), and even in ie7 the situation gets cleaned up when the user navigates away from the page (although JS that creates a lot of "throwaway" DOM elements could still get into trouble). So, most programmers really have to worry about this only if still targeting ie6.

(By the way, if you are afflicted with ie6, the jQuery 'data' APIs work around this misbehavior; other libraries often have similar tricks.)

The official scoop from Microsoft: http://msdn.microsoft.com/en-us/library/dd361842%28v=vs.85%2...


Indeed. Also, a slightly more succinct fix for IE6/7 would be:

  function foo(element, a, b) {
    element.onclick = function() { /* uses a and b */ };
    element = null; // avoid memory leak
  }
The closure contains a reference to foo's activation object, not to its properties. As a consequence, you can clear out the element reference after the event handler has been defined and this will break the circular reference.

The above technique is also useful when creating anonymous functions for other purposes when element references happen to be lying around. Depending on how long these anonymous functions are kept alive and what you do with the elements in the meantime, this would normally lead to a memory leak (due to the dangling reference to the DOM element, rather than a circular reference as described in the article).

See http://jibbering.com/faq/notes/closures/ for a seriously nerdy deep-dive into closures in JavaScript.


Indeed. Last I checked, jQuery breaks the cycle when elements are removed from the DOM by going through all their event handlers and data members and dropping them.


I don't think jQuery is necessarily relevant in this discussion. This post is talking about JavaScript closures in general, regardless of whether or not it's DOM events or plain old callbacks.


(Edit: I think I'm wrong here. It does indeed look like the OP is saying that the DOM node would leak after the cycle has no incoming pointers. I read it to say that other objects in the closed-over scope would be leaked as long as the reference was live).

I don't think the OP is confused. Nothing he has said is inaccurate, and you're asserting something he didn't say.

> A proper garbage collector will collect cyclic structures.

Absolutely true, so long as they are garbage. However, nothing the OP says implies that the DOM node is garbage. As I read it, the leak comes from accidentally extending the life of the enclosing scope to the life of the DOM node. That's a real leak (in the web app, not in the browser).


In every browser except IE4-7, removing the DOM node from the document would result in the entire lot getting garbage collected. This is garbage collection collecting garbage. However, in IE4-7 the JS garbage collector isn't responsible for handling DOM nodes and vice versa. Consequently, this results in a memory leak; in IE 4-6 this persists until the browser terminates, and in IE7 it persists until the user navigates to another page.

There is a related application-level memory leak if you have some other reference to the closure that doesn't belong to the DOM node, and that applies to all browsers. See http://news.ycombinator.com/item?id=2567847 for a simple way to deal with that.


> However, in IE4-7 the JS garbage collector isn't responsible for handling DOM nodes and vice versa.

It's a little sad that you don't expand on this, for those in the audience who do not know of this issue. So I will:

In IE up to IE7, Javascript lives in the jscript engine (equivalent to one V8, Tamarin, Rhino or what have you), but DOM objects are actually COM objects. COM objects are handled with a COM runtime, and there is an interface through which jscript interacts with COM.

MSIE4-7 has no issue with jscript circular references (it uses a mark & sweep GC, immune to the issue), and the COM engine ensures there are no issues with circular references in the DOM (COM uses refcounting but — as in Python for instance — has cycle detections).

Issues, as usual, arise at the boundary: if a jscript object has a reference to a (COM) DOM node, or a DOM node has a reference to a jscript object, it creates a foreign reference which will not be cleaned up until the holder object on the other side of the interface is done with.

For both garbage collectors, those foreign references are completely opaque, the only thing they know is that those exist but they have no way to know what happens on the other side of the jscript-COM interface. This means if there is a cycle crossing the boundary (twice), neither garbage collector is able to clean it up. And there is no Breach, able to take care of boundary issues such as this.

And it leaks.

A lot.

In fact, it leaks so much that a few years ago a tool was tested with the sole purpose of reloading a page and carefully monitoring the objects which fail to be cleaned: drip.


The OP never mentions removing the node from the DOM.


It's kind of implied, isn't it? I mean, it wouldn't make sense to expect something that's still part of the DOM tree to get garbage collected, would it? Unless I'm missing something?


You're right. My initial reading was the closure closed over extra objects (which would be a leak so long as the DOM node is live), but I think the OP is worried about the DOM node leaking.


It was an assumption I made in my reply (now corrected) but it doesn't matter; even without removing the node from the DOM there's a memory leak in IE4-6, one that persists until the browser is terminated.

In the other cases, there's no memory leak while the DOM node still exists, because the DOM still has a reference to it so it shouldn't be garbage collected.


I'm not a JavaScript expert, and so I didn't understand this bit:

> create a circular reference and thus, a memory leak

It was confusing because JavaScript uses mark-and-sweep collection, which can handle circular references just fine.

A bit of googling turns up this link, which explains it:

http://www.ibm.com/developerworks/web/library/wa-memleak/

The problem is that the DOM is reference counted (at least in some browsers, at least back when that article was written). So, you can get memory leaks in cases where JS intersects with the DOM.


> so I didn't understand this bit:

That's because it's wrong.

> It was confusing because JavaScript uses mark-and-sweep collection, which can handle circular references just fine.

I don't think that's mandated, you could probably implement a refcounted JS interpreter.

> The problem is that the DOM is reference counted (at least in some browsers, at least back when that article was written). So, you can get memory leaks in cases where JS intersects with the DOM.

And the main culprit was, as is usually the case, Internet Explorer.


There is a better explanation at the very end of this awesome description of javascript's closures and scope resolution: http://jibbering.com/faq/notes/closures/


This is a well-known issue that we shouldn't still be discussing in 2011. Any sane JavaScript implementation already addresses this issue, and any responsible JavaScript framework will address this issue for you for the sake of older browsers (e.g. see the jQuery source at https://github.com/jquery/jquery/blob/master/src/event.js#L1...).

To say that we all do this is more than a bit of an exaggeration.


It's definitely a stretch to say we all run into these issues, but you'd be surprised how many beginner and intermediate-level developers don't know about this issue precisely because they've been insulated from it by frameworks.

There are plenty of situations where developers can find themselves dealing with insane JavaScript implementations and irresponsible JavaScript frameworks. While the article doesn't explain exactly what the issue is and which browsers it affects (leading me to believe the author himself doesn't truly understand these memory leaks), the recommendation given is a sound one.


> It's definitely a stretch to say we all run into these issues, but you'd be surprised how many beginner and intermediate-level developers don't know about this issue precisely because they've been insulated from it by frameworks.

Or by most browsers not being broken anymore.

> which browsers it affects

Internet Explorer up to version 7.


Has this actually been tested? Does it really create a leak, and does the second code really eliminate it?

And with the way Chrome handles tabs (in separate processes) does it really matter, since closing the tabs releases the memory?


> Has this actually been tested? Does it really create a leak, and does the second code really eliminate it?

it does not create a leak in any modern browser. The only older browser in which it does is Internet Explorer, up to (and including) version 7.


Using jquery will avoid this issue, because it automatically removes event handlers when a node is taken out of the DOM tree.


See the internet explorer memory leak section at the end of this article for a full explanation: http://jibbering.com/faq/notes/closures/


Just a quick point, but rather than using the DOM0 interface, use the DOM2 addEventListener method.


I don't really get the difference, in both cases the a,b will be stored somewhere no? I mean, when you return that function it references a and b as the original function would have and you've done absolutely nothing different.

Can someone explain this a little better?


The argument is that because in the first case, the closure is created in a scope where element, a, and b all exist. So element is available in the closure even if it's never used.

Since now element has a property which closes over element, he argues this is a circular reference, which will eventually become a memory leak.

It seems to me that a smart JS engine could avoid this by noticing that there's no possibility of using element within the first closure, but I'm not an expert. Also it seems like a small price to pay unless you are constantly creating and destroying such widgets; surely anything associated with an unloaded page can be GC'ed regardless of circular references?


> It seems to me that a smart JS engine could avoid this by noticing that there's no possibility of using element within the first closure, but I'm not an expert.

They don't even need that. Any garbage collection strategy (including refcounting) have no trouble reclaiming cycle (the only GC type which may have problems are refcounting GCs, and they usually have a cycle detector for exactly that purpose).

The only way for this issue to arise is to involve two runtimes, each with its own garbage collector, able to create references between one another.

A cycle crossing the boundary will not be detectable by either GC, and will lead to a leak.

This is exactly what happens in Internet Explorer up to (and including) version 7: JavaScript lives in the jscript engine and the DOM lives in COM, each has its own GC, and they can have references to objects living in the other runtime.


And all of it stems from the fact that, JavaScript being a dynamic language, there's no way to statically determine whether the function will use 'element' or not (it could always eval() something evil and sneaky). Most functional languages would avoid packing 'element' at compile time, and I suspect that modern JavaScript compilers also handle this optimization whenever they can afford it.


I've found that when I use the webkit inspector, those unused values don't show in the Closure section of the Scope Variables display, and if I put a breakpoint in there and try to reference the unused variables I get a Reference Error.


So the issue is only with DOM elements in IE 6, right? If I write:

    function foo(huge) {
        some_global_thing.call_later = function() { 42 }
    }
And call foo with "huge", huge is not saved in call_later's closure, right? And it doesn't change if I do:

    function foo(huge, x){
        blah_blah_blah.whatever = function() { x + 1 };
    }
We're still only closing over "x", right?


Unfortunately, eval() complicates things. Consider:

    function foo (x) {
      return function(what) {
        return eval(what);
      };
    }
    
    var theClosure = foo(42);
    var theX = theClosure("x");
    document.write(theX); // displays 42
Even though "x" is never explicitly used, it's still accessible through the eval'd expression.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: