4

I am trying to understand why the following code causes a memory leak

var aThing = null;
var outer = function() {

    console.log('running');
    var something = aThing;

    var closure1 = function() {
        if (something) {
            console.log('something');
        }
    };

    aThing = {
        str: new Array(1000000).join('8'),
        someMethod: function() {}
    };
};
setInterval(outer, 1000);

Here is the timeline showing memory increasing from Google Chrome:

Memory Leak

but this code which is a very slight variation does not cause the same memory leak:

var aThing = null;
var outer = function() {

    console.log('running');
    var something = aThing;
    var closure1 = function() {
        if (something) {
            console.log('something');
        }
    }

    aThing = {
        str: new Array(1000000).join('8')
    };

    function someMethod() {};
};
setInterval(outer, 1000);

Here is the equivalent timeline showing that GC is cleaning up OK.

No leak

I understand that in the first version there is a memory leak because the variable 'something' does not get cleaned up. Why is it being GC'ed in the second example but not the first?

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
user3391835
  • 305
  • 1
  • 3
  • 14
  • 1
    1. Avoid changing more than one thing. Even though it's almost certainly irrelevant, don't swap the order of the first two statements in `outer` **and also** do something else. Figuring these things out requires making **one** change at at time and eliminating the irrelevant. 2. When asking for help, please take the time to indent the code readably and consistently. I've fixed #2 for you. – T.J. Crowder Dec 27 '15 at 13:08
  • 2
    OP, interestingly @T.J.Crowder [writes about garbage collection here](http://stackoverflow.com/questions/4324133/how-does-garbage-collection-work-in-javascript) which might help you out a bit. – Andy Dec 27 '15 at 13:20

1 Answers1

3

The primary answer is that in your second code block, no direct reference to either of the closures (closure1 or someMethod) survives the return of outer (nothing outside outer refers to them), and so there's nothing left that refers to the context where they were created, and that context can be cleaned up. In your second code block, though, a direct reference to someMethod survives the return, as part of the object that you're assigning to aThing, and so the context as a whole cannot be GC'd.

Let's follow what happens with your first block:

After the first execution of outer, we have (ignoring a bunch of details):

            +−−−−−−−−−−−−−+
aThing−−−−−>| (object #1) |     
            +−−−−−−−−−−−−−+     
            | str: ...    |     +−−−−−−−−−−−−−−−−−−−−+
            | someMethod  |−−−−>| (context #1)       |
            +−−−−−−−−−−−−−+     +−−−−−−−−−−−−−−−−−−−−+
                                | something: null    |
                                | closure1: function |
                                +−−−−−−−−−−−−−−−−−−−−+

after the second execution:

            +−−−−−−−−−−−−−+
aThing−−−−−>| (object #2) |     
            +−−−−−−−−−−−−−+     
            | str: ...    |     +−−−−−−−−−−−−−−−−−−−−+     
            | someMethod  |−−−−>| (context #2)       |     
            +−−−−−−−−−−−−−+     +−−−−−−−−−−−−−−−−−−−−+     +−−−−−−−−−−−−−+                           
                                | something          |−−−−>| (object #1) |                           
                                | closure1: function |     +−−−−−−−−−−−−−+                           
                                +−−−−−−−−−−−−−−−−−−−−+     | str: ...    |     +−−−−−−−−−−−−−−−−−−−−+
                                                           | someMethod  |−−−−>| (context #1)       |
                                                           +−−−−−−−−−−−−−+     +−−−−−−−−−−−−−−−−−−−−+
                                                                               | something: null    |
                                                                               | closure1: function |
                                                                               +−−−−−−−−−−−−−−−−−−−−+

after the third execution:

            +−−−−−−−−−−−−−+
aThing−−−−−>| (object #3) |     
            +−−−−−−−−−−−−−+     
            | str: ...    |     +−−−−−−−−−−−−−−−−−−−−+     
            | someMethod  |−−−−>| (context #3)       |     
            +−−−−−−−−−−−−−+     +−−−−−−−−−−−−−−−−−−−−+     +−−−−−−−−−−−−−+                                                                          
                                | something          |−−−−>| (object #2) |                                                                          
                                | closure1: function |     +−−−−−−−−−−−−−+                                                                          
                                +−−−−−−−−−−−−−−−−−−−−+     | str: ...    |     +−−−−−−−−−−−−−−−−−−−−+                                               
                                                           | someMethod  |−−−−>| (context #2)       |                                               
                                                           +−−−−−−−−−−−−−+     +−−−−−−−−−−−−−−−−−−−−+     +−−−−−−−−−−−−−+                           
                                                                               | something          |−−−−>| (object #1) |                           
                                                                               | closure1: function |     +−−−−−−−−−−−−−+                           
                                                                               +−−−−−−−−−−−−−−−−−−−−+     | str: ...    |     +−−−−−−−−−−−−−−−−−−−−+
                                                                                                          | someMethod  |−−−−>| (context #1)       |
                                                                                                          +−−−−−−−−−−−−−+     +−−−−−−−−−−−−−−−−−−−−+
                                                                                                                              | something: null    |
                                                                                                                              | closure1: function |
                                                                                                                              +−−−−−−−−−−−−−−−−−−−−+

You can see where this is going.

Since the second block never retains a reference to closure1 or someMethod, neither of them keeps the context in memory.

When originally answering your question in 2015 I was slightly surprised that V8 (Chrome's JavaScript engine) didn't optimize this leak away, since only someMethod is retained, and someMethod doesn't actually use something or closure1 (or eval or new Function or debugger). Although in theory it has references to them via the context, static analysis would show that they can't actually be used and so could be dropped. But closure optimization is really easy to disturb, I guess something in there is disturbing it, or that the V8 team found that doing that level of analysis wasn't worth the runtime cost. I do recall seeing a tweet from one of the V8 team saying that it used to do more closure optimization than it does now (this edit is in Sep 2021) because the trade-off wasn't worth it.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • 1
    Thanks a lot @T.J. Crowder, the diagrams are especially helpful in understanding this. FYI the reason that `someMethod` is retained is because `closeure1` refers to originalThing. As soon as _any_ closure refers to a local variable this variable will be in the context for all closures. If you remove `closure1` it will not leak, even though `closure1` is never used! – user3391835 Dec 27 '15 at 14:33
  • @user3391835: That's the theory, but again, because nothing actually accesses `closure1`, it could be optimized. The theory is that locals are held in an object, and closures have a reference to that object. The reality in modern engines is, of course, much more complicated. :-) – T.J. Crowder Dec 27 '15 at 14:40
  • @T.J.Crowder, great explanation! Please allow me to ask one more question, if closure1 does not survive anyway when outer function returns, then why remove it(closure1) would fix the leak issue? – chen.w Sep 18 '21 at 05:38
  • @chen.w - Sorry, that was poor wording on my part, I've updated the answer a bit. :-) – T.J. Crowder Oct 31 '21 at 13:24