4

I've been trying to track down any issues with garbage collection within my application code. I've stripped it down to pure knockout code and there seems to be an issue collecting created objects depending on how a computed property is created.

Please see the following JS fiddle: http://jsfiddle.net/SGXJG/

  1. Open Chrome profiler.
  2. Take a heap snapshot
  3. Click Make All
  4. Take another heap snapshot
  5. Compare the snapshots
  6. Test2 & Test3 still remain in memory while Test1 is correctly collected.

Please see the following code for the viewmodel:

function ViewModel() { this.test1 = null; this.test2 = null; this.test3 = null; } ViewModel.prototype = { makeAll: function () { this.make1(); this.make2(); this.make3(); }, make1: function () { this.test1 = new Test1(); this.test1.kill(); delete this.test1; }, make2: function () { this.test2 = new Test2(); this.test2.kill(); delete this.test2; }, make3: function () { this.test3 = new Test3(); this.test3.kill(); delete this.test3; }, }; ko.applyBindings(new ViewModel()); 

And here are the three tests classes:

function Test1() { var one = this.one = ko.observable(); var two = this.two = ko.observable(); this.three = ko.computed(function () { return one() && two(); }); } Test1.prototype = { kill: function () { this.three.dispose(); } }; function Test2() { this.one = ko.observable(); this.two = ko.observable(); this.three = ko.computed(function () { return this.one() && this.two(); }, this); } Test2.prototype = { kill: function () { this.three.dispose(); } }; function Test3() { var self = this; self.one = ko.observable(); self.two = ko.observable(); self.three = ko.computed(function () { return self.one() && self.two(); }); self.kill = function () { self.three.dispose(); }; } 

The difference being that Test1 'three' computed does not use this or self to reference the 'one' & 'two' observable properties. Can someone explain what's happening here? I guess there's something in the way the closures contain the object reference but I don't understand why

Hopefully I've not missed anything. Let me know if I have and many thanks for any responses.

2
  • I should clarify. I believed, even though Test2 & Test3 instances still reference the computed which references back to the instance, that they would have been sufficiently orphaned from the ViewModel 'main program' to be garbage collected as a group. Does that make sense? Commented Mar 29, 2014 at 11:18
  • I posted this question on google groups (groups.google.com/forum/#!topic/knockoutjs/6vbJ45T5f08) which prompted Michael Best to reply to this question. Thought it might be worth sharing as it may provide a little more insight & background. Commented Apr 30, 2014 at 15:49

3 Answers 3

2

Chrome, like all modern browsers, uses a mark-and-sweep algorithm for garbage collection. From MDN:

This algorithm assumes the knowledge of a set of objects called roots (In JavaScript, the root is the global object). Periodically, the garbage-collector will start from these roots, find all objects that are referenced from these roots, then all objects referenced from these, etc. Starting from the roots, the garbage collector will thus find all reachable objects and collect all non-reachable objects.

The garbage collector doesn't run right away, which is why you still see the objects in Chrome's snapshot even though you've dereferenced them (edit: As mentioned here, running the heap snapshot first runs the garbage collector. Possibly it's not processing everything and so doesn't clear the objects; see below.)

One thing that seems to generally trigger the garbage collector is to create new objects. We can verify this using your example. After going through the steps in your question, click on "Make1" and take another heap snapshot. You should see that Test2 is gone. Now do it again, and you'll see that Test3 is also gone.

Some more notes:

  1. Chrome doesn't clean up everything during garbage collection. "[Chrome]...processes only part of the object heap in most garbage collection cycles." (google.com) Thus we see that it takes a couple runs of the garbage collector to clear everything.

  2. As long as all external references to a object are cleared, the object will be cleaned up by the garbage collector eventually. In your example, you can even remove the dispose calls, and all the objects will be cleared.

Sign up to request clarification or add additional context in comments.

4 Comments

developers.google.com/chrome-developer-tools/docs/… states unreachable objects are cleaered and that GC is run when a snapshot is taken. I will investigate your answer tomorrow however. Also my question on Google groups was just only related to this. Not the same question but I will investigate based on this answerr. But for now it's 2:45am and I should be asleep. Thanks!
Thanks for the link. I've added it to the answer.
Thanks Michael I confirm Test2 disappeared. For whatever reason Test3 would not go away (stating it was holding onto the prototype from the window object) so I added Test3.prototype = {}; and it behaved as you stated. This is indeed the behaviour I had expected following my own reading but obviously I was not patient enough with the GC to do it's rounds and see it happen.
The statement on Q&A "Are "dead" (unreachable) objects included in snapshots? No. Only reachable objects are included in snapshots. Also, taking a snapshot always starts with doing a GC." gave me a false impression that everything would be collected that could be collected.
2

I think this is a classical loop reference problem.

Let's call:

var test2 = new Test2(); 

now test2.three holds a reference of test2! Because you literally asked knockout to bind a function(){...} with that "this" object, the test2 object.

Since test2 naturally holds a reference of test2.three, you now got a loop reference between the two objects!

You can see this is same for your Test3.

But for Test1, let's call:

var test1 = new Test1(); 

test1.three holds references of two objects (test1.one and test2.two), test1 holds three references (test1.one, test1.two and test1.three), there is no loop reference.

In some other languages like Java and Objective-C, the language supports weak reference to deal with this kind of issue. But so far, weak reference not implemented in Javascript.

+1 thanks for your question! It gave my brain some spin, helped me to understand Javascript more :)

10 Comments

you may wonder why test3.three holds reference of "self", not "self.one" and "self.two". It is because in js, self.one() is literally just self['one']().
Thanks for the answer. I believe you are correct in what you say but I'm not sure if that answers why the objects Test2 and Test3 are not GC'd. I have removed the reference to the created instances in the viewmodel using delete this.test2 and delete this.test3 should it not be the case that the instance objects are orphaned for GC even though they have an internal referencing loop?
:) garbage collector has no way to deal with loop reference. You can google this topic. Your delete this.test2 only remove the ref of test2 hold by viewModel object.
This answer is wrong. Take a look at the answer I provided.
I am glad to see Michael's explain on js mark-and-sweep too. Javascript's root object architect makes it easy to find dependancy of the objects in the tree. I was not aware about it.
|
0

I think the problem is that you use && in your code, it will return a boolean, properly "true".

this.three = ko.computed(function () { // ! return this.one() && this.two(); }, 

So this.three == true and not self.one + self.two if this was the intention? and when you dispose

this.three.dispose(); 

You just get rid of a boolean.

Is there a reason why you have an extra "this" in "function Test2()"?

2 Comments

Thanks for your answer. This.one and this.two are functions which at at any point could return booleans. three is a computed which has a dispose function property. I do not believe this is my issue.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.