You really can't make blanket statements about appropriate way to use all GC implementations. They vary wildly. So I'll speak to the .NET one which you originally referred to.
You must know the behaviour of the GC pretty intimately to do this with any logic or reason.
The only advice on collection I can give is: Never do it.
If you truly know the intricate details of the GC, you'll not need my advice so it won't matter. If you don't already know with 100% confidence it will help, and have to look online and find an answer like this: You should not be calling GC.Collect, or alternatively: You should go learn the details of how the GC works inside and out, and only then will you know the answer.
There is one safe place it makes some sense to use GC.Collect:
GC.Collect is an API available that you can use for profiling timings of things. You could profile one algorithm, collect, and profile another algorithm immediately afterwards knowing GC of the first algo wasn't occurring during your second one skewing the results.
This sort of profiling is the single time I would ever suggest manually collecting to anyone.
Contrived Example Anyway
One possible use case is if you load really large things, they'll end up in the Large Object Heap which will go straight to Gen 2, though again Gen 2 is for long lived objects because it collects less frequently. If you know that you are loading short lived objects into Gen 2 for any reason, you could clear them out more quickly to keep your Gen 2 smaller and it's collections faster.
This is the best example I could come up with, and it's not good - the LOH pressure you're building here would cause more frequent collections, and collections are so frequent as it is - chances are it would be clearing out the LOH just as fast as you were blowing it out with temporary objects. I simply don't trust myself to presume a better collection frequency than the GC itself - tuned by people far far smarter than I.
So let's talk about some of the semantics and mechanisms in the .NET GC... or..
Everything I think I know about the .NET GC
Please, anyone who finds errors here - do correct me. Much of the GC is well known to be black magic and while I tried to leave out details I was uncertain of, I probably still got some things wrong.
Below is purposely missing numerous details I'm not certain about, as well as a far larger body of information I'm simply unaware of. Use this information at your own risk.
GC Concepts
The .NET GC occurs at inconsistent times, which is why it's called "non-deterministic", this means you can't rely on it to occur at specific times. It's also a generational garbage collector, which means it partitions your objects into how many GC passes they've lived through.
Objects in Gen 0 heap have lived through 0 collections, these have been newly made so recently no collection has occurred since their instantiation. Objects in your Gen 1 heap have lived through one collection pass, and likewise objects in your Gen 2 heap have lived through 2 collection passes.
Now it's worth noting the reason it qualifies these specific generations and partitions accordingly. The .NET GC only recognizes these three generations, because the collection passes that go over these three heaps are all slightly different. Some objects may survive collection passes thousands of times. The GC merely leaves these on the other side of the Gen 2 heap partition, there's no point in partitioning them anywhere further because they're actually Gen 44; the collection pass on them is the same as everything in Gen 2 heap.
There are semantic purposes to these specific generations, as well as implemented mechanisms that honor these, and I'll get to those in a moment.
What's in a collection
The basic concept of a GC collection pass is that it checks each object in a heap space to see if there are still live references (GC roots) to these objects. If a GC root is found for an object, it means currently executing code can still possible reach and use that object, so it cannot be deleted. However if a GC root is not found for an object, it means the running process no longer needs the object, so it can remove it to free up memory for new objects.
Now after it's finished cleaning up a bunch of objects and leaving some alone, there will be an unfortunate side effect: Free space gaps between live objects where the dead ones were removed. This memory fragmentation if left alone would simply waste memory, so collections will typically do what's called "compaction" where they take all the live objects left and squeeze them together in the heap so the free memory is contiguous on one side of the heap for Gen 0.
Now given the idea of 3 heaps of memory, all partitioned by the number of collection passes they've lived through, let's talk about why these partitions exist.
Gen 0 Collection
Gen 0 being the absolute newest objects, tends to be very small - so you can safely collect it very frequently. The frequency ensures the heap stays small and the collections are very fast because they are collecting over such a small heap. This is based more or less on a heuristic that claims: A large majority of temporary objects which you create, are very temporary, so temporary they'll no longer be used or referenced almost immediately after use, and thus can be collected.
Gen 1 Collection
Gen 1 being objects that didn't fall into this very temporary category of objects, may still be rather short lived, because still- a vast portion of the objects created are not used for long. Therefore Gen 1 collects rather frequently as well, again keeping it's heap small so it's collections are fast. However the assumption is less of it's objects are temporary than Gen 0, so it collects less frequently than Gen 0
I will say I frankly don't know the technical mechanisms that differ between Gen 0's collection pass and Gen 1's, if there are any at all other than the frequency they collect.
Gen 2 Collection
Gen 2 now must be the mother of all heaps right? Well, yes, that's more or less right. It's where all your permanent objects live - the object your Main()
lives in for instance, and everything that Main()
references because those will be rooted until your Main()
returns at the end of your process.
Given that Gen 2 is a bucket for basically everything the other generations couldn't collect, it's objects are largely permanent, or long lived at the least. So recognizing very little of what's in Gen 2 will actually be something that can be collected, it doesn't have need to collect frequently. This allows it's collection to also be slower, since it executes so much less frequent. So this is basically where they've tacked on all the extra behaviours for odd scenarios, because they have the time to execute them.
Large Object Heap
One example of the extra behaviours of Gen 2 is that it also does the collection on the Large Object Heap. Up until now I've been talking entirely about the Small Object Heap, but the .NET runtime allocates things of certain sizes to a separate heap because of what I referred to as compaction above. Compaction requires moving objects around when collections finish on the Small Object Heap. If there's a living 10mb object in Gen 1, it's going to take far longer for it to complete the compaction after collection, thus slowing down Gen 1's collection. So that 10mb object is allocated to the Large Object Heap, and collected during Gen 2 which runs so infrequently.
Finalization
Another example is objects with finalizers. You put a finalizer on an object that references resources beyond the scope of .NETs GC (unmanaged resources). The finalizer is the only way the GC gets to demand an unmanaged resource is collected - you implement your finalizer to do the manual collection/removal/release of the unmanaged resource to ensure it doesn't leak from your process. When the GC gets to executing your objects finalizer, then your implementation will clear the unmanaged resource, making the GC capable of removing your object without risking a resource leak.
The mechanism with which finalizers do this is by being referenced directly in a finalization queue. When the runtime allocates an object with a finalizer, it adds a pointer to that object to the finalization queue, and locks your object in place (called pinning) so compaction won't move it which would break the finalization queue reference. As collection passes occur, eventually your object will be found to no longer have a GC root, but the finalization must be executed before it can be collected. So when the object is dead, the collection will move it's reference from the finalization queue and place a reference to it on what's known as the "FReachable" queue. Then the collection continues on. At another "non-deterministic" time in the future, a separate thread known as the Finalizer thread will go through the FReachable queue, executing the finalizers for each of the objects referenced. After it's finished, the FReachable queue is empty, and it has flipped a bit on the header of each object that says they don't need finalization (This bit can also be flipped manually with GC.SuppressFinalize
which is common in Dispose()
methods), I also suspect it has unpinned the objects, but don't quote me on that. The next collection that comes around on whatever heap this object is in, will finally collect it. Gen 0 collections don't even pay attention to objects with that finalization-needed bit on, it automatically promotes them, without even checking for their root. An unrooted object needing finalization in Gen 1, will get tossed on the FReachable
queue, but the collection doesn't do anything else with it, so it lives into Gen 2. In this way, all objects which have a finalizer, and don't GC.SuppressFinalize
will be collected in Gen 2.