To GC.Collect or not?
Reading this old but classic document Writing High-Performance Managed Applications - A Primer, I came across following statment
The GC is self-tuning and will adjust itself according to applications memory requirements. In most cases programmatically invoking a GC will hinder that tuning. "Helping" the GC by calling GC.Collect will more than likely not improve your applications performance
I am working with applications that during a given point in time, consumes a lot of memory. When I am done in code consuming that memory, I am calling GC.Collect. If I don't do it I get Out of memory exception. This behaviour is inconistent but roughtly speaking 30% of the time, I get an out of memory. After adding GC.Collect I never got this out of memory exception. Is my action justified even though this best practice document is advising against it?
Part of what goes on in the GC is that objects in memory are generational, such that early generations are collected more frequently than others. This helps save performance by not trying to collect long-lived objects all the time.
With that in mind, two things can happen when you call GC.Collect() yourself. The first is that you end up spending more time doing collections. This is because the normal background collections will still happen in addition to your manual GC.Collect(). The second is that you'll hang on to the memory longer, because you forced some things into a higher order generation that didn't need to go there. In other words, using GC.Collect() yourself is almost always a bad idea.
There are a few cases where the garbage collector doesn't always perform well. One of these is the large object heap. This is a special generation for objects larger than a certain size (80,000 bytes, IIRC, but that could be old now). This generation is hardly ever collected and almost never compacted. That means that over time you can end up with many sizable holes in memory that will not be released. The physical memory is not actually used and is available for other processes, but it does still consume address space within your process, of which you are limited to 2GB by default.
This is a very common source for OutOfMemory exceptions — you're not actually using that much memory, but you have all this address space taken up by holes in the large object heap. By far the most common way this happens is repeatedly appending to large strings or documents. This probably is not you, because in this scenario no amount of calls to GC.Collect() will companct the LOH, but in your case it does seem to help. However, this is the source for the vast majority of the OutOfMemory exceptions I've seen.
Another place where the garbage collector does not always perform well is when certain things cause objects to remain rooted. One example is that event handlers can prevent an object from being collected. A way around this is make sure that every += operation to subscribe an event has a corresponding -= operation to unsubscribe it. But again, a GC.Collect() is unlikely to help here - the object is still rooted somewhere, and so can't be collected.
Hopefully this gives you an avenue of investigation to solve your underlying problem that causes the need to use GC.Collect() in the first place. But if not it is, of course, better to have a working program than a failing program. Anywhere I do use GC.Collect(), I would make sure the code is well documented with the reason why you need it (you get exceptions without) and the exact steps and data required to reproduce it reliably so that future programmers who may want to remove this can know for sure when it is safe to do so.
Most people would say that making your code work correctly is more important than making it fast. Thus, it it fails to work 30% of the time when you don't call GC.Collect(), then that trumps all other concerns.
Of course, that leads to the deeper question of "why do you get OOM errors? Is there a deeper issue that should be fixed, instead of just calling GC.Collect().
But the advice you found talks about performance. Do you care about performance if it makes your app fail 30% of the time?
Generally speaking, GC.Collect shouldn't be necessary. If your images exist in unmanaged memory, then be sure to use GC.AddMemoryPressure and GC.RemoveMemoryPressure appropriately.
From your description it sounds like Disposeable objects are not being disposed, or you're not setting member values that will be replaced to null before the operation. As an example of the latter:
- Get table, display in grid
- (User hits refresh)
- Form is disabled while data is refreshed
- Query comes back, new data is populated in the grid
You can clear out the grid in the interim since it's about to be replaced anyway; you will temporarily have both tables in memory (unnecessarily) while it is replaced if you don't.