15

Take a look at this part of a Chrome heap snapshot:

Chrome circular reference

It shows the retainers of an object in the heap that, as far as I know and can see, should be garbage, but is not collected despite that.

The "shortest" path to the root is, after all, a cyclical path (it never actually reaches the root). Which leaves one to wonder, how is the snapshot viewer even able to assign a distance of 12 to it? Is that just the number of steps it took through the cycle before giving up? Note how the distance never gets below 11.

I've read that it can take a few iterations to clean up subgraphs with circular references. But repeated forced collections (with the trash can button in the Timeline tab) failed to clean up these objects.

Note that exploring through the '185' references eventually leads to the same system / Context @862399, so there really isn't a path from the root to this object (at least not visible here).

Am I going crazy, or is the garbage collector actually broken? I don't remember having this issue in the past. I'm on Chrome 45.0.2454.101. Beta 46.0.2490.64 behaves the same.

Bart van Heukelom
  • 43,244
  • 59
  • 186
  • 301
  • Are these methods even able to be garbage collected? Are you creating closures? Could you inadvertently be creating a memory leak? – Jesse Oct 13 '15 at 03:59
  • The [Google JavaScript style guide](https://google.github.io/styleguide/javascriptguide.xml?showone=Closures#Closures) has an example of a closure that creates a circular reference resulting in a memory leak. If this is the case of the OP, then it's nothing new: [1](http://stackoverflow.com/questions/19550253), [2](http://stackoverflow.com/questions/16442201), [3](http://stackoverflow.com/questions/11186750). Not sure why but modern JS engines don't seem to have a solution to address this very common source of memory leaks. – GOTO 0 Oct 13 '15 at 08:15
  • @GOTO0 I'm aware that a closure keeps references to enclosing variables it uses (how could it function otherwise?) but that it also retains non-used enclosing variables is new to me, and actually pretty disappointing. However, that style guide says "a circular reference and thus, a memory leak". Is that not an outdated statement that only applies to reference counting GC? I thought mark-and-sweep was supposed to make this a non-issue? And if that part is wrong, is the rest of that guide trustworthy? – Bart van Heukelom Oct 13 '15 at 09:06
  • @GOTO0 I've done a little test: http://pastebin.com/fPxNxqSy. Even though the closure returned by `x` is retained by `window`, the `MyClass` instance `m` created there is not retained (as seen in a heap snapshot). The one in `y `is retained, because it cannot be optimized away due to `eval`. (This is an implementation detail in V8, and while I suspect other modern browsers behave the same, I haven't tested that.) – Bart van Heukelom Oct 13 '15 at 11:46
  • That Google style code link is just someones suggestions, albeit someone from google, so with any suggestion article, you should take it as such. There are some well known older articles about this that are evidence based with awesome recommendations and examples. However the best documentation you are going to find is going to be your chrome profile and memory usage. Using your application for a bit and then leaving it open for a bit untouched will help you identify *your* opportunities for memory management more than an article or my/any answer can. – Jesse Oct 13 '15 at 11:56
  • I mean, this is a good question, it always is, the answer is always the same though, garbage collection does what it can, all you can do is try and optimize the best you can. It of course sucks sometimes having to do this. Garbage collection is like your hoarder neighbor. It's going to keep everything it think you will need later and sometimes if it sees the right thing, it keeps that too. Try deleting a variable declared with `var`, good stuff. – Jesse Oct 13 '15 at 12:02
  • Well yes, if the garbage collector doesn't work, I'll have to help it along, since I can't just hope for and/or wait until its improved and deployed. I am working on that. While that's going on, with this question, I'm trying to understand *why* it doesn't work. The behaviour of the GC is clearly defined: clean up that which can't be referenced through a GC root. As far as I can see, the above objects can't, and I can only conclude either the GC itself or the heap viewer is broken, or my understanding of either is wrong. – Bart van Heukelom Oct 13 '15 at 17:16
  • Probably a little bit of all 3. V8 is totally open source and as the saying goes -> if you don't like it, fork it https://code.google.com/p/v8/ – Jesse Oct 13 '15 at 18:36

1 Answers1

2

To be completely honest we would need a quick look at some test code where you replicated this but I have a general idea of what you are experiencing. If I am wrong and you can provide some test code that proves this, please let me know.

As you seem to already know, to 'clean up', Javascript wants no more references to the item looking to be freed.

A simple example:

// Currently not eligible for garbage
var myObj = {
    data : 'test'
};

// Reference data inside myObj
// A unique identifier to myObj.data now exists 
var myData = myObj.data;

// Whoops
myObj = 'something else';

// Because myData exists, so does data and can not be freed either, understandable
// This sounds simple and logical but most do not understand what is happening in the engine
// A relationship has been born between 'myData' and 'data' and as long as it exists, it is not eligible for garbage and remains in memory
console.log( myData );

Your code is probably more complicated than this but this might help explain how, somewhere, the garbage can not be collected as the scope chain can be followed to a reference.

Consider the following

function oops(text){

    function doStuff(){
        return text.toLowerCase();
    }
    return doStuff();
}

// 'doStuff' stays alive thanks to 'test' below.
var test = oops('closure');

the function doStuff will not be garbage collected because it is being referenced by test.

// You can see where this is headed. We have 2 references to the same 'doStuff' function object with separate unique identifiers now below.
var test2 = oops('closures...');

// This is now another unique identifier to 'doStuff'
var test3 = test2;

// doStuff survives yet still
test = 0;
test2 = 0;

// Now we let the function object of 'doStuff' be freed because there is no longer any references to it
test3 = 0;

This is essentially a memory leak we created. Each time you call oops you are creating a function object doStuff with a unique identifier.

A way to avoid this could be

function doStuff( text ){
    return text.toLowerCase();
}

function oops( text ){
    return doStuff();
}

var test = oops( 'closure' );

Now we have no memory leaks. doStuff is being invoked and not created.

A closer look at your code and you will find you are probably doing this somewhere.

If you are messing with elements and I think you might be, IBM has a good article about circular references you may want to take a look at.


It's late and some of this was untested, but the theory is still there so if I misspelled something etc, let me know and I can take a look tomorrow for future visitors to this page.

Jesse
  • 2,790
  • 1
  • 20
  • 36
  • Thanks for your answer, but as commented above, I was already aware of the retaining power of closures, though apparently not fully. Still, if the values in my heap snapshot are reachable from a GC root, why isn't there a full path shown with decreasing distance down to 1? Does this mean the snapshot viewer is actually not to be trusted? And as said in the question, I didn't experience this problem in the past (though it's hard to say whether a change in the app or Chrome caused this). – Bart van Heukelom Oct 13 '15 at 09:11
  • I've tried reproducing the problem in sample code, but didn't succeed. It should be said that the real application in which it occurs is rather large, being a sizable single-page game. Its memory usage (before leaks) can reach several hundred megs. I don't know if browser are quite ready for that yet? – Bart van Heukelom Oct 13 '15 at 09:13
  • Sure man, browsers can handle a few hundred megs if the memory usage is justified but the more memory the higher the latency. If I may recommend something. Take a snapshot, use your application for a bit and take another and compare. With dom elements you may find places to optimize as the person did in this article:: https://blog.newrelic.com/2012/07/17/using-chrome-developer-tools-to-find-memory-leaks/ – Jesse Oct 13 '15 at 11:42
  • I think your example with the inner function is wrong and it won't leak – catamphetamine Apr 30 '16 at 23:17
  • @asdfasdfads As it is creating instances of a function each time the outer function is called, it is bad. – Jesse Apr 30 '16 at 23:22
  • Well, it crates `doStuff()` every time but this function also is destroyed soon after due to exiting out of the scope of the parent function so there's no memory leak. – catamphetamine May 01 '16 at 17:47
  • 1
    Maybe Jesse accidentally wrote "return doStuff()". I think what he meant was to "return doStuff" (without doStuff invocation) – NicBright Aug 26 '16 at 14:11
  • Thank you for pointing that out. I should have returned with the text variable or without (). It was late at night when I answered this question last year. – Jesse Aug 26 '16 at 17:03