Generational Garbage Collection
We all dread the OOM (Out of Memory) exception in .Net. Why does the application run out of memory? Why is garbage collection not "working"?
The .Net CLR's Garbage Collector is a generational garbage collector. What does this mean? Well, specifically in the case of .Net the CLR has 3 generations:
- Generation 0
- Generation 1
- Generation 2
Each generation has an allocation budget. This budget is chosen by the CLR based on a number of factors including available memory in the environment.
At the very start of an application before the first garbage collection, everything starts out in the GC Heap. When garbage collection runs (it can and does target specific generations) it will remove what it can from the Heap. Items that cannot be disposed will be moved to the next generation. This can be seen in Visual Studio's Diagnostic Tools:
Approximately 3MB were removed from the Heap when GC ran. 1 MB was added to Generation 0 and when it's budget as allocated, some was moved to Generation 1. Each higher generation can hold more memory, but consequently is garbage collected less frequently than lower generations.
every time garbage collection runs, anything that survives garbage collection on a particular generation is moved to the next generation. That promotion of items to the next generation can trigger garbage collection on that next generation if the new allocation exceeds the budget of that generation.
For example, in the image below the first time garbage collection runs it moves items from the Heap to Generation 1 and Generation 2. When garbage collection runs for the second time, some items are successfully disposed on Generation 0, and everything in Generation 1 is promoted to Generation 2. Generation 2 is normally garbage collected the least frequently.
Upon heavy use of a system, if garbage collection fails to reclaim available memory from a generation, the CLR can allocate more available memory to it, if there is more available.
A few notes of interest:
- Garbage Collection is much quicker and more efficient in smaller, lower generations than the larger, higher generations.
- Garbage Collection is triggered by the CLR when it thinks it's appropriate. This can be when the budgeted allocation for a generation is exceeded, when the allocator can no longer allocate on given segment within a generation, or when there is ample idle time for the runtime to schedule a higher generation collection.
- Garbage Collection should almost never be triggered programmatically.
What are things you can do to reduce GC?
Some simple things that can be done to reduce GC allocation are:
- String ComparisonUsing
//USE String.Equals(string1, string2, StringComparison.OrdinalIgnoreCase); //NOT string1.ToLowerInvariant() == string2.ToLowerInvariant();
ToLowerInvariant
will cause a new string to be allocated on the heap, whereasString.Equals(string1, string2, StringComparison.OrdinalIgnoreCase)
causes no extra heap allocations and thus less garbage collection later, not to mention a lot faster. - String Concatenation - when doing a lot of string concatenation, use
StringBuilder
to avoid unnecessary string allocations on the heap, and again, a lot faster.
Reduce Boxing and Unboxing
Boxing is the process of converting a value type to the type
object
or to any interface type implemented by this value type. Boxing and Unboxing are expensive operations and boxing in particular must create a new object that is then allocated to the heap.int i = 123; object o = i; //boxes i, assigns to `object` implicitly i = (int)o; // unboxing, explicit cast from object to type `int`
Be careful, some methods might implicitly box values:
Know when to use a
struct
instead of aclass
. Structs can avoid additional heap allocations, but should only be used in certain circumstances. From the Microsoft docs:- CONSIDER defining a struct instead of a class if instances of the type are small and commonly short-lived or are commonly embedded in other objects.
- AVOID defining a struct unless the type has all of the following characteristics:
- It logically represents a single value, similar to primitive types (int, double, etc.).
- It has an instance size under 16 bytes.
- It is immutable.
- It will not have to be boxed frequently.
Tools
Benchmarks: In .Net use BenchmarkDotNet with
[MemoryDiagnoser]
to benchmark GC and memory allocations between various implementations.Debug memory leaks with Visual Studio Diagnostics Tools or JetBrains dotMemory Or use Microsoft's
dotnet-counter
anddotnet-dump
tools to debug memory leaks.
Resources
https://github.com/dotnet/runtime/blob/main/docs/design/coreclr/botr/garbage-collection.md