Garbage Collection in C# (.NET Framework and .NET Core)
Posted by: Damir Arh
in Category C#
Abstract: This C# tutorial explains how Garbage Collection works in .NET Framework and .NET Core, and some best practices to follow.
Garbage Collection in .NET Framework
Garbage collection makes automatic memory management in .NET framework possible. Thanks to it, the developer is only responsible for allocating memory by creating new instances of objects. Allocated memory is automatically released by the garbage collector once the created objects are not used any more.
All managed objects in the .NET framework are allocated on the managed heap. They are placed contiguously as they are created. If this process would continue like that for a long time, we would eventually run out of memory. To prevent that, garbage collection gets triggered when memory allocations reach a certain total size or when there’s a lack of available memory.
The garbage collection consists of three phases:
- In the marking phase, a list of all objects in use is created by following the references from all the root objects, i.e. variables on stack and static objects. Any allocated objects not on the list become candidates to be deleted from the heap. The objects on the list will be relocated in the compacting phase if necessary, to compact the heap and remove any unused memory between them.
- In the relocation phase, all references to remaining objects are updated to point to the new location of objects to which they will be relocated in the next phase.
- In the compacting phase, the heap finally gets compacted as all the objects that are not in use any more are released and the remaining objects are moved accordingly. The order of remaining objects in the heap stays intact.
To better handle different lifetimes of different objects, garbage collection introduces the concept of generations. A newly created object is placed in generation 0. If it is not released during a garbage collection run, it is moved to generation 1. If it is still not released when the next garbage collection run happens, it is moved to generation 2. It stays there for the remainder of its lifetime.
Based on the assumption that most objects are only used for a short time, while the objects that are used longer are less likely to be released soon if at all, the generations are used to optimize the garbage collection process. Most garbage collection runs will only process generation 0 and will therefore take the shortest time. Once more memory needs to be released, generation 1 will be included in garbage collection as well, making it take more time. When even generation 1 garbage collection doesn’t release enough memory, generation 2 gets included as well. This is called full garbage collection and takes the longest time.
Figure 1: Garbage collection releases unused objects and promotes used objects through generations
To further optimize garbage collection, there is a separate object heap for objects larger than 85,000 bytes. Objects placed in it are handled differently:
- New objects are immediately put into generation 2, since larger objects (usually arrays) tend to live longer.
- Objects are not moved at the end of garbage collection to compact the large objects heap because moving larger objects would be more time consuming.
There are two different types of garbage collection:
- Workstation garbage collection runs on the normal priority thread, which triggered it by passing a memory threshold with its latest memory allocation or by explicitly calling GC.Collect
- Server garbage collection creates a separate managed heap and a corresponding garbage collection thread for each logical CPU. Threads run on highest priority. This approach to garbage collection makes it faster but more resource intensive.
By default, workstation garbage collection is used, but this can be changed with a setting in the application configuration XML file (named AssemblyName.config):
<?xml version="1.0" encoding="utf-8"?>
If the computer only has a single logical CPU, the setting will be ignored and the workstation garbage collection type will be used.
Each approach to garbage collection is available in three different variations:
- Non-concurrent garbage collection suspends all non-garbage-collection threads for the full duration of the garbage collection, effectively pausing the application for that time.
- Concurrent garbage collection allows user threads to run for the most of generation 2 garbage collection. As long as there is still free space in the managed heap for new allocations, user threads are allowed to run and create new objects. This results in a shorter garbage collection pause, at the cost of higher CPU and memory requirements.
- Background garbage collection is the replacement for concurrent garbage collection. It was introduced in .NET framework 4 for workstation garbage collection, and in .NET framework 4.5 for server garbage collection. It is similar to concurrent garbage collection but allows generation 0 and generation 1 garbage collection to interrupt an ongoing generation 2 garbage collection and temporarily block program execution. After generation 0 and generation 1 garbage collection is completed, both the program execution as well as the generation 2 garbage collection, continues. This even further shortens the time the program execution is paused because of garbage collection.
By default, concurrent or background garbage collection will be used (depending on the .NET framework version), but this can be changed with a setting in the application configuration file:
<?xml version="1.0" encoding="utf-8"?>
Garbage Collection in .NET Core
The same two garbage collection configuration options are available .NET Core. Since there’s no application configuration XML file in .NET Core, the settings are placed in the runtime configuration JSON file (named AssemblyName.runtimeconfig.json) instead:
Typically, this file is not edited manually. Its contents are generated at compile time based on the MSBuild project (.csproj) file. To configure the garbage collector, you can set the following properties in the project file:
Garbage Collection - Best Practices
Taking all this knowledge into account, application performance can be improved by following these recommendations when garbage collection is identified as the cause for bad performance:
- When a multithreaded application creating a lot of objects is running on a computer which doesn’t host many other processes, switch from workstation to server garbage collection. The application will benefit from shorter pauses while not negatively affecting other applications with high priority garbage collection threads.
- Although garbage collection can be triggered manually by calling GC.Collect, it should usually be avoided as the CLR heuristics will be able to better schedule garbage collection based on all the information available. Probably the only use case when manual garbage collection could make sense would be when in a short time, a lot of objects were not in use, and the application can afford a deterministic garbage collection pause without negatively affecting the user.
- Allocation of large objects with short life time should be avoided as much as possible. It will take a long time before they are released because they are immediately placed in generation 2 and they will make the large object heap fragmented because it is not compacted.
- If possible, avoid allocating large numbers of objects with short lifetimes and try to reuse the objects instead. This will avoid frequent triggering of generation 0 garbage collection, which causes pauses in program execution.
This article was technically reviewed by Yacoub Massad.
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!