I thought garbage collection was suppose to automatically get rid of things after all their references are gone, what gives?
This is incorrect in two ways.
Garbage collection collects managed memory that it is safe to collect, and is needed. That's it.
Think of garbage collection as a way to simulate infinite heap memory. Since we can pretend we have infinite memory, we never have to call anything to free memory we have finished using, because why would we preserve an infinite resource?*
The simplest way for the GC to simulate infinite heap, is to do nothing. This works when e.g. the process has 4GiB of memory available, and it uses 50MiB. Collecting never comes up. This does indeed happen if an app is small enough that collections never happen. (Though it's not so lazy as to let you use megs without a collection, it'll try collecting before it tries asking the OS for more memory for the app, still it can be useful when you are wondering "why didnt' the GC..." that doing nothing can be a valid GC approach some of the time, once you've got that possibility in mind, a lot of those other questions go away).
Another approach is to eagerly clean up everything the moment it can be cleaned up. This happens with reference-counting garbage collectors; which has nothing to do with .NET but is worth mentioning because it would closely match the "after all their references are gone" of your question.
Another is that when memory is needed, and not available in the store of already free memory, then the GC stops all threads, identifies roots (static variables and the local references within the stack of each thread), identifies everything referenced by a root, and everything referenced by those, and so on until it's found everything that could still feasibly be used by the application, and then considers the memory taken by everything else as free. It then compacts all the objects that didn't have their memory freed, which both avoids fragmentation when giving out more free memory, and also often keeps objects closer together in memory (which has minor performance benefits).
If also "promotes" the objects it didn't delete, as chances are if it wasn't correct to delete it the first time, it won't be the next time, so it will look at those objects that survived this process less often.
Two things to note at this point:
- We cannot predict when, if ever, the GC will free the memory of a given object.
- The only thing the GC does is free managed memory. It doesn't do anything else (it does help with finalisers, which we'll come to later).
Obtaining and then freeing managed memory is of course only one case where we may wish to first do something, and then undo it. Other examples are:
- Obtaining a file handle, and releasing it.
- Obtaining a windows handle, and then releasing it.
- Obtaining a GDI handle, and then releasing it.
- Opening a network connection, and then closing it.
- Sending a protocol-defined handshake over a network connection, and then sending a protocol-defined sign-off over it before it is closed.
- Obtaining some unmanaged memory, and then freeing it.
- Obtaining an object (for which there is some overhead beyond allocation to its creation, or it "learns" through being used) from a pool, and then returning it to the pool.
GC as we have described it so-far will not help any of these.
Still, they all have two features in common. They have a starting operation, and a ending operation.
The starting operation will either map well to object creation, or to some method call.
The ending operation can match to a Close()
, End()
, Free()
, Release()
method call, but in defining IDisposable.Dispose()
we can give them all a common interface. The language can also add some help then through using
†.
(A class may have both a Close()
and a Dispose()
. In this case we have both the option of closing something we will later re-open or otherwise use in its closed state, and also the method of guaranteeing clean-up after we are finished with the object).
So in this way, IDisposable.Dispose()
exists to clean-up all the stuff that needs cleaned-up except for managed memory.
Now, in this case, there are three types of class that need to implement IDisposable
:
- Those which hold the sort of unmanaged resource like a handle, themselves.
- Those we are using with some sort of pooling or other before/after scenario of our own devising (or someone else's devising, but still all within .NET itself).
- Those which have fields which in turn implement
IDisposable
and therefore when we dispose the object of this class, it calls Dispose()
on those fields.
Let's think about what happens if the GC frees the memory of such an object, and Dispose()
hadn't been called.
In the third case, it doesn't actually matter, that the object wasn't disposed. What really matters is that the fields weren't disposed (or perhaps that doesn't matter, but some field down the line matters).
In the second case, how much it matters depends on how important the pooling was. It's probably sub-optimal, but not the end of the world.
In the first case, it's a disaster - we've an unreleased resource that we cannot possibly release until the application ends, and perhaps even after that (depending on the nature of the resource).
For this reason, objects can have finalisers.
When the GC is about to free the memory of an object, if it has a finaliser, and if that finaliser hasn't been suppressed (Dispose()
will normally do this to show that the object is nicely cleaned up and no more work is needed on it), then instead of freeing the memory from the object, it will put it into the finalisation queue. This of course means not only does that object not have its memory collected, but nor does any reachable through its fields.
A finaliser thread works its way through this queue, calling the finaliser method on each of them.
There are two bad things about this happening:
- We don't know when it will happen. Maybe we'll run out of a resource or be unable to open a file for writing before it does.
- It means that an object that should have had its memory freed is promoted instead, and will live not just one cycle longer than it should have, but many cycles longer.
Edit: Note that we do not have a finaliser on classes of the third kind, and maybe not of the second kind. In this case the finaliser isn't needed because the really crucial object it had as a field will have its finaliser called, which does the important work. It's also really easy to end up with an atrocious bug if you try to deal with a finalisable field from within a finaliser. If you write a disposable class that wraps one or more disposable fields it "owns" and has responsibility for cleaning up, then implement IDisposable
, but do not add a finaliser.
In all, a finaliser being called means one of two things:
- The application is shutting down and all finalisers are being run (grand, all is well with the world).
- Someone messed up and didn't clean up something when they should have.
So, while there is an interaction between GC and resources other than managed memory via finalisers, it's an interaction of last resort and in no way whatsoever is it dependable. You shouldn't think of finalisers as a way to make the GC do clean-up, but as a way for the GC to not make clean-up impossible if a flaw meant it didn't happen (and for a way to have clean-up on application shut-down).
*Of course, if you think you have an infinite resource (fish, buffalo, waste-processing capacity of oceans) and it turns out you don't, then things can get messy, so maybe don't apply that rule to everything in life.
†using
makes calling Dispose()
simpler in that
using(someDisposableObject)
{
//Do Stuff
}
Is equivalent to:
try
{
//Do Stuff
}
finally
{
if(someDisposableObject != null)
((IDisposable)someDisposableObject).Dispose();
}
And:
using(var someDisposableObject = someMethodCallOrCallToNew())
{
//Do Stuff
}
Is equivalent to:
var someDisposableObject = someMethodCallOrCallToNew();
try
{
//Do Stuff
}
finally
{
if(someDisposableObject != null)
((IDisposable)someDisposableObject).Dispose();
}
The null-check may be removed in cases where the compiler can determine that it's impossible for someDisposableObject to be null, as an optimisation.
The reason that IDisposable
exists is to support exceptional cases where the garbage collection can't [effectively] manage an objects memory. The primary reason for it's existence is for releasing unmanaged resources.
One of the common cases of this is interaction with unmanaged memory. When an object is doing something that involves allocating memory outside of the scope of the garbage collector then the garbage collector can't be responsible for cleaning it up; it needs to be handled by the programmer. These cases also have a higher probability of allocating large amounts of memory, making it that much more important to ensure that it is cleaned up more quickly.
It is also used in cases where an object has some sort of "clean up" that it needs to do other than actually freeing memory. Examples are file handers when dealing with IO, or connections to a database that should be closed. Having the garbage collector free the memory for the object won't perform this type of cleanup.
There are also cases where programmers use the IDisposeable
interface to "hijack" onto the syntax for the using
statement and don't actually have unmanaged resources disposed in it's dispose method.