JavaScript Memory leak from closure lexical enviro

2019-07-10 01:53发布

问题:

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:

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.

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?

回答1:

The primary answer is that in your second code block, neither 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, someMethod survives the return, as part of the object that you're assigning to aThing, and so the context 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.

I'm slightly surprised that V8 (Chrome's JavaScript engine) doesn'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), so 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.