Garbage Collector
Understand how the .NET garbage collector manages memory allocation, generations, and automatic cleanup of unreachable objects.
We'll cover the following...
There is a significant difference in how value and reference types are stored in memory. When we create a local value-type variable, the value behind the variable is stored on the method stack. However, if a value type is a member of a class, it is stored on the heap along with the object. When the method finishes running, the stack is cleared.
Local reference-type variables are also stored on the stack, but the variable contains the memory address of the region in the heap where the object is actually stored.
When a method finishes running, reference-type variables are immediately deleted, but the objects they pointed to are not cleared instantly. They are deleted automatically sometime later by the garbage collector.
Garbage collector
When there are no references pointing to an object, this object becomes unreachable and is eligible for garbage collection. The garbage collector reclaims memory occupied by unreachable objects. The garbage collector does not run every time a reference is deleted from the stack. The CLR launches the garbage collector only when necessary, such as when the application is running out of memory.
Objects in the heap are not always stored sequentially. Gaps can form between objects, leaving the heap fragmented. For optimization, a process called compaction typically follows garbage collection, moving remaining objects into a contiguous block of memory. References update with new memory addresses so that links between variables and objects do not break.
Notes on memory
For objects larger than 85,000 bytes, there is a special memory area called the large object heap (LOH). Historically, the main difference from the regular heap was that the large object heap was never compacted, because moving large objects in memory is time-consuming and can degrade application performance. In modern .NET, the LOH is still not compacted by default, but we can instruct the garbage collector to compact it on demand.
Compaction takes time, and the application pauses until the process is complete. However, the benefits obtained from memory optimization outweigh the potential disadvantages. To reduce its performance footprint, the garbage collector does not scan the entire application memory every time it collects unreachable objects. Different areas of memory are scanned at different intervals based on their generation:
Generation 0 holds newly created objects, except for those larger than 85,000 bytes. The garbage collector begins its scan in this area of memory. Generation 0 objects that survive the collection are automatically moved to Generation 1.
Generation 1 is only scanned if more memory is needed after the garbage collector finishes scanning Generation 0. Objects that survive the scan are moved to Generation 2.
Generation 2 is for objects with the longest lifespan in the application. The garbage collector rarely scans this part of memory. This area is only scanned when memory is still needed after Generation 1 is scanned.
The GC class
The garbage collector’s functionality is exposed to .NET developers through the System.GC class. We can send commands to the garbage collector using its static methods. Most often, the GC class is never used directly because garbage collection is automatic for managed objects in .NET.
Note: Managed objects are all objects created within the context of the CLR.
We might call the garbage collector manually in specific scenarios, such as when our code works with unmanaged resources. Also, if our code constantly creates and uses large objects, we might want to call the garbage collector manually so that Generation 1 and 2 objects are cleared more often.
Here are some of the methods of System.GC:
AddMemoryPressure(): This informs the CLR that a large chunk of memory was allocated for an unmanaged resource. This information enables the CLR to better determine when it is time to launch garbage collection.Collect(): This launches the garbage collection process. There are multiple overloads that allow us to control which generations must be scanned.GetGeneration(object): This shows the generation an object currently belongs to.GetTotalMemory(): This returns the amount of memory, in bytes, currently in use in the managed heap.WaitForPendingFinalizers(): This blocks the current thread until all objects being collected are completely cleared from memory.
Let us discuss the Collect() method in greater detail. There are overloads that allow us to fine-tune the collection process. For instance, we can indicate which generations must be scanned: