15 December

In-Depth – Memory Management in C#

Programming

min. read

Reading Time: 7 minutes

Memory management is an important aspect of any programming language, and C# is no exception. In this article, we will discuss how memory management works in C# and how you can use various techniques and tools to manage it effectively in your C# applications.

If you wish to get into even more details please visit Microsoft documentation on this topic: https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/

Computer memory
Computer memory

When a C# program runs, the operating system allocates memory for it. This memory stores data and objects created during the program’s execution. The C# runtime manages this memory and ensures its efficient use. It does so through garbage collection, which automatically frees up memory no longer in use by the program.

One way that C# manages memory is through the use of garbage collection. The garbage collector is a system that automatically frees up memory that is no longer being used by the program.

Memory Garbage Collection

Garbage collection (GC) is a mechanism in C# and other programming languages that automatically frees up memory that is no longer being used by the program. This is an important part of memory management in C#, as it allows programmers to focus on the logic of their programs without having to worry about manually freeing up memory.

The garbage collector is responsible for identifying objects in the program’s memory that are no longer being used by the program. It does this by periodically scanning the memory and marking any unused objects for deletion. Once these objects are marked, the garbage collector frees up the memory that they were occupying, making it available for use by other objects. This process is known as garbage collection.

There are two main types of garbage collection in C#: mark-and-sweep and reference counting.

Mark-and-sweep GC

Mark-and-sweep garbage collection works by first marking all of the objects in the program’s memory that are currently being used by the program. It then sweeps through the memory and frees up any objects that were not marked. This type of garbage collection is efficient, but it can cause temporary pauses in the program’s execution while the garbage collection is taking place.

Reference counting GC

Reference counting garbage collection, on the other hand, works by maintaining a count of the number of references to each object in the program’s memory. When the reference count for an object reaches zero, the object is no longer being used by the program and its memory is freed up. This type of garbage collection does not cause pauses in the program’s execution, but it can be less efficient than mark-and-sweep garbage collection.

C# implementation of GC

In C#, the garbage collector uses a combination of mark-and-sweep and reference counting to manage memory efficiently and avoid pauses in the program’s execution.

Implementation details

In C#, the garbage collector is implemented as part of the .NET runtime. The .NET runtime is responsible for managing the execution of C# programs and provides various services, including memory management.

The garbage collector in C# is a non-deterministic, compacting, mark-and-sweep garbage collector. This means that it runs at non-deterministic times (i.e. it is not guaranteed to run at a specific time or interval), it compacts the memory used by the program to reduce fragmentation, and it uses mark-and-sweep to free up memory.

The garbage collector in C# uses a technique called generational garbage collection to improve its performance. This technique divides objects in the program’s memory into different generations, based on how long they have been in memory. The youngest generation holds new objects, and as the program runs and the objects survive garbage collections, they are promoted to older generations. The garbage collector runs more frequently on the younger generation because these objects are more likely to be garbage, while the older generations are collected less frequently. This allows the garbage collector to quickly free up the memory used by short-lived objects, while still collecting the memory used by long-lived objects.

The garbage collector in C# is triggered automatically when the program’s memory usage reaches a certain threshold. This threshold is called the low-memory threshold, and it is determined by the .NET runtime based on the amount of available memory on the system. When the garbage collector is triggered, it begins a garbage collection cycle. You can also nudge the .NET environment to trigger GC event, see more in the “Manual GC” section.

During a garbage collection cycle, the garbage collector first stops all managed threads (i.e. threads that are executing C# code) in the program. This is necessary because the garbage collector needs to have exclusive access to the program’s memory in order to perform its operations. While the managed threads are stopped, the garbage collector runs a mark phase, where it marks all of the objects in the program’s memory that are currently being used by the program. It then runs a sweep phase, where it frees up any objects that were not marked. Finally, it compacts the memory used by the program to reduce fragmentation.

Once the garbage collection cycle is complete, the managed threads are resumed and the program continues execution. The garbage collector will run again when the program’s memory usage reaches the low-memory threshold.

Mark and sweep

Imagine that we have a C# program that uses four objects: obj1, obj2, obj3, and obj4. The memory used by the program looks like this:

[obj1][obj2][obj3][obj4]

Now, imagine that obj2 and obj4 are no longer being used by the program. During the mark phase of the garbage collection cycle, the garbage collector would mark all of the objects in the program’s memory that are currently being used by the program. In this case, only obj1 and obj3 are being used, so they would be marked, like this: (Added ! for marking)

[obj1][!obj2][obj3][!obj4]

In the sweep phase of the garbage collection cycle, the garbage collector would then free up the memory used by the objects that were not marked, which in this case would be obj2 and obj4. The memory used by the program would then look like this:

[obj1][free memory][obj3][free memory]

Compacting Phase

During a garbage collection cycle in C#, the garbage collector runs a compacting phase in addition to the mark and sweep phases. The purpose of the compacting phase is to reduce fragmentation in the program’s memory, which can improve the performance of the program.

Fragmentation occurs when the memory used by the program is not contiguous (i.e. it is not one continuous block of memory). This can happen when objects are created and deleted during the execution of the program, as the memory used by the deleted objects is not immediately reclaimed. As a result, the program’s memory becomes fragmented, with gaps between blocks of memory that are in use.

The compacting phase of the garbage collection cycle works by moving the objects in the program’s memory so that they are contiguous (i.e. they form one continuous block of memory). This reduces the gaps between blocks of memory and makes the program’s memory more efficient.

During the compacting phase, the garbage collector moves the objects in the program’s memory to new locations in memory. It does this by updating the references to the objects in the program’s code to point to the new locations. This ensures that the program can continue to access the objects correctly, even after they have been moved.

The compacting phase of the garbage collection cycle can improve the performance of the program by reducing fragmentation and making the program’s memory more efficient. However, it can also cause some temporary performance issues, as moving the objects in the program’s memory requires additional processing. Therefore, the garbage collector tries to balance the benefits of compacting with the potential performance impact.

Imagine that we have a C# program that uses three objects: obj1, obj2, and obj3. The memory used by the program looks like this:

[obj1][obj2][obj3][free memory]

Here, [obj1], [obj2], and [obj3] represent the blocks of memory used by the three objects, and [free memory] represents a block of unused memory.

Now, imagine that obj2 is deleted during the execution of the program. The memory used by the program now looks like this:

[obj1][free memory][obj3][free memory]

As you can see, the memory used by the program has become fragmented, with a gap between [obj1] and [obj3]. This can cause performance issues, as the program’s memory is not being used efficiently.

During the compacting phase of a garbage collection cycle, the garbage collector would move the objects in the program’s memory so that they are contiguous, like this:

[obj1][obj3][free memory]

This reduces the fragmentation in the program’s memory and makes it more efficient. The references to the objects in the program’s code would also be updated to point to the new locations of the objects, so that the program can continue to access them correctly.

Manual GC Memory Events

In C#, the garbage collector is a non-deterministic garbage collector. This means that it runs automatically at non-deterministic times (i.e. it is not guaranteed to run at a specific time or interval), and it is not possible to manually schedule a garbage collection event.

However, it is possible to force the garbage collector to run by calling the GC.Collect() method. This method allows you to specify which generation of objects should be collected (i.e. the youngest generation, the oldest generation, or all generations).

For example, to force the garbage collector to collect the objects in the youngest generation, you can use the following code:

GC.Collect(0);

To force the garbage collector to collect the objects in the oldest generation, you can use the following code:

GC.Collect(2);

And to force the garbage collector to collect all objects, regardless of their generation, you can use the following code:

GC.Collect();

It is generally not recommended to call the GC.Collect() method, as it can cause performance issues. The garbage collector is designed to run automatically at non-deterministic times, and it is usually best to let it do its job without interference. However, in certain situations, calling the GC.Collect() method may be necessary, such as when you want to ensure that certain objects are collected before a specific point in the program’s execution.

Analytics and Performance

One of the common use cases for manual GC events can be to get how much “trash” we generated, between GC events.

In order to do that we can get the current memory footprint, of the application by GC.GetTotalMemory, requesting a Collection event and testing for total memory again.

// Put some objects in memory.
var before = GC.GetTotalMemory(false);
Console.WriteLine($"Memory used before collection: {before:N0}");

// Collect all generations of memory.
GC.Collect();
var after = GC.GetTotalMemory(true);
Console.WriteLine($"Memory used after full collection: {after :N0}");
Console.WriteLine($"GC Memory freed: {(before - after):N0}");

// The output from the example resembles the following:
//       Memory used before collection:       79,392
//       Memory used after full collection:   52,640
//       GC Memory freed:                     26.752

Was this interesting? Check out our new C# 11 features blog series


Let's talk

SEND THE EMAIL

I agree that my data in this form will be sent to [email protected] and will be read by human beings. We will answer you as soon as possible. If you sent this form by mistake or want to remove your data, you can let us know by sending an email to [email protected]. We will never send you any spam or share your data with third parties.

I agree that my data in this form will be sent to [email protected] and will be read by human beings. We will answer you as soon as possible. If you sent this form by mistake or want to remove your data, you can let us know by sending an email to [email protected]. We will never send you any spam or share your data with third parties.