.NET Memory: My object is not rooted, why wasn’t it garbage collected?

4 minute read

I got a comment on one of the posts asking this question and I started writing a comment-answer but it turned into a long-winded rant so I decided to blog it instead:)

So you’re looking at a dump and run !gcroot on your object but it doesn’t find a root. Then why is it still around on the heap…

There are many reasons for this but the short answer is: It was still alive (rooted) last time a garbage collection for that specific generation was run.

This is not completely true… it could be that there is a problem with !gcroot in this specific case causing it to not find the root but this would be pretty rare. It could also be that you are running the workstation version where only partial collections are done if the garbage collection take too long, since the workstation GC is optimized for applications with UI and we don’t want to block the UI threads causing the UI to flicker.

Short of this, we can go back to “the object was alive during the last collection” and take a look at some of the cases for this

Garbage collection didn’t run yet for this generation

Garbage collection is allocation triggered except for in a few cases such as the app manually calling GC.Collect() or the ASP.NET cache reduction mechanism for example.

What does this mean?

Simplified, each generation (0, 1, 2 and large object) has a limit. I.e. how much data can be stored in each generation before a garbage collection occurs. These limits are dynamically changed based on the applications memory usage.

When the application allocates a new object and the limit for generation 0 is exceeded, a gen 0 GC is triggered. Objects in use (rooted) are then moved to Gen 1. If this causes the Gen 1 limit to be exceeded a Gen 1 GC is triggered and objects in-use move to Gen 2 etc. Gen 2 is currently the max generation. Large objects are treaded separately.

So let’s say you perform a stress test, and then leave the server idle for 4 hours with no requests, this means that no new allocations are made and thus no new GCs are triggered so the memory used for the process will never get reduced. In essence, this does not mean that you have a memory leak, it just means that you are not triggering any new GCs. A proper stress test should have some kind of slow-down period after the main stress.

Going back to the objects that are not rooted…

So now we know that the most likely reason is that a garbage collection of that generation has not occurred since the object was un-rooted.

Your object needs finalization

Another alternative is that the object has a finalizer method, and thus is registered (and rooted) with the finalizer and will therefore be held until the finalizer thread gets around to finalizing it. Or it could be a member variable of an object with a finalizer.

This would not show up in !gcroot, but you can see the object show up in !finalizequeue.

Implementing a finalize method (even if there is no code in it) will automatically put your object on the finalize queue which means that your object will survive at least one garbage collection, therefore you should carefully consider if your object really needs a finalizer.

Worst case scenario your finalizer might be blocked so it will take a long time for your object to be finalized if ever… (run !threads to identify the finalizer thread and check if perhaps it is stuck when finalizing objects). I have on my todo list to write a “case study” on blocked finalizers.

Your object is a ‘large object’

Another alternative is that the object you are looking at is a “large object”, or a member variable of a “large object”. Garbage collection of the large object heap is much more infrequent than the small object heap, which means that any object that is stored on the large object heap may stay around for a substantial amount of time.

Induce collections to test

Finally, if you have a repro, you can try calling

GC.Collect(3)
GC.WaitForPendingFinalizers()
GC.Collect(3)

and see if your object is still around after executing this.

This will garbage collect all generations (including large object), then execute any finalizers, and then garbage collect again to take care of all the objects that had finalizers.

The specific question in the comment was “could this be because it is used in interop?”

The answer is no, with interop if your object was still in-use it would be rooted in a refcount, or as a pinned object or similar, depending on how it was created and used.

Note: this is by no means an exhaustive list of all reasons, it’s just the most common ones that came to my mind when reading the comment, but at least hopefully it should somewhat explain why you see un-rooted objects on the heap…

I would recommend that you take a peak at Maoni’s blog on using the GC efficiently if you want an interesting read on what the GC does.

Happy debugging!