December 4th, 2019 | by Konrad Zajda

Memory Management in .NET - Garbage Collector

Table of contents

We, as .NET developers, do not think about memory management in .NET a lot. We think about Garbage Collector even less. And that’s fantastic. Garbage Collector has been designed to make it useful without even thinking about allocating and deallocating memory when it is no longer needed. The majority of developers don’t bother with collecting garbage as far as they don’t run into trouble with it.

Before we dive into how garbage collection works in .NET, let’s remember how it looks in old, but still good, C++. Let’s assume we want to create a variable, which holds the username and displays it on the screen. Our code will look somehow like that:

#include #include void showUsername() { char *username; username = (char *) malloc(8); strcpy(username, "username"); printf("%s", username); } int main() { showUsername(); }

When the variable which holds the username is no longer needed, we need to free the memory taken by it by calling:

free(username);

Meanwhile in C# we do:

var username = “username”; Console.WriteLine(username);

See the difference? Business logic is missing somewhere between memory management pieces of code. But it’s getting more complicated when we have more functions, classes, and variables. How can we know that memory taken by a variable is no longer needed and can be freed? Without proper Garbage Collection, we run into serious issues with code readiness and memory leaks.

Memory leak might occur when we anyhow release memory, which is still used, or we do not release memory, which is not needed anymore (we eventually get into “out of memory” issues quickly). Does it mean that we won’t have memory leaks with the Garbage Collector? Of course, we will. We can cause memory leaks in .NET in many different ways, for example, by not unsubscribing to events, or by caching resources infinitely. That’s called Managed Memory Leak.

Deep dive into .NET Garbage Collector

In .NET, Garbage Collector is part of Common Language Runtime (there are more Garbage Collector versions, but we keep our focus CLR here) and its source code can be found on Github (with only two classes, more than 1MB and almost 40000 lines of code, it is not cleanest code if you ask me). To explain the essentials of Garbage Collector, we need to explain some terms to be on the same page.

When is the Garbage Collector being triggered?

A really common mistake done by beginning .NET developers is the answer to the question “How long will this variable live in the memory?”. Some people think that Garbage Collection will be triggered after leaving some scope, and every variable from that scope will be collected then. The truth is, it won’t. It will be run with memory allocation (or manually, by calling GC.Collect()). That’s because the Garbage Collector is pretty smart, and it won’t collect anything as far as it’s really needed.

Allocator

When you want to assign a value or reference to the variable, you need to allocate the memory necessary for that assigned value. Basically, it’s about writing it under some memory address. The allocator is the one who is responsible for allocating memory for new objects. If we consider a simple scenario, where we have some memory allocated for objects called O1, O2, …, On at the end of it, we have Next Object Pointer, which points to the memory address where the next object will be allocated. After allocating that memory, the pointer will be moved just right after the On + 1 object. Meanwhile, in C++ for example, we need to find free memory with size at least big as a resource, which costs more.

CSHARK
CSHARK

Tracing

Core CLR Garbage Collector uses a Tracing algorithm, which contains three different stages:

  • Marking

Before we can deallocate memory, we need to find out which memory is no longer used. GC is doing that by marking, which is looking for objects no longer referenced in other objects and marking them as ready to deallocate.

CSHARK
CSHARK
  • Compacting

After we find out which objects are no longer needed, we can make more space for a new one. Compacting is removing all these dead objects and reallocating them within a single, continuous area by moving them next to each other.

CSHARK
CSHARK
  • Sweeping

If we want to free memory to cost less, we can do sweeping, which just deallocate memory marked in the marking stage. But be aware, it makes our memory fragmented (see where the next object pointer is).

CSHARK
CSHARK

Generations and Object Heaps

Every .NET developer who heard about Garbage Collector at least once also heard about something called Generations and Heaps.

We have two heaps in the Garbage Collector: Small-Object Heap (which contains generation 0, generation 1, and generation 2) and Large-Object Heap (which contains all objects larger than 85’000 bytes).

CSHARK
CSHARK

Small-Object Heap

Whenever Garbage Collector is being run in SOH, it marks and compacts or marks and sweeps (whichever suits best at that moment). Every new object is allocated in generation 0 and will be promoted to generation 1 when, after collecting garbage, there is some reference to this object. Of course, the same thing with generations 1 and 2.

Because generations 0 and 1 are in the cache memory, cleaning them is way more efficient than cleaning generation 2 and large-object heap. Microsoft claims it takes no longer than 1 millisecond.

CSHARK
CSHARK

Large-Object Heap

Before .NET 4.5, LOH used to use simple allocator, as illustrated above with Mark & Sweep only. Nowadays, it’s a bit smarter, and it looks for free memory to avoid fragmentation, similar to what’s done in C++. We are talking about objects which are at least 85’000 bytes, so it could be fragmented a lot. Fortunately, after .NET 4.5.1 we can do single Compact on demand, by calling simple two lines of code:

GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; GC.Collect();

But because we are still talking about Large-Object Heap, compacting might take a while, so be careful as it might freeze your application for a moment.

Summary

Hopefully, we don’t need to think about all of these on a daily basis. But it’s still good to know how memory management works in .NET. Thanks to that, we can focus on delivering business value without remembering when and how we should allocate or deallocate our memory. Knowledge about Garbage Collector is often really useful during interviews, and now you know how it works.

Konrad Zajda

A Fullstack Developer with .NET Core at CSHARK. Delivering user-focused solutions with the newest technologies. Passionate about software quality, low latency systems, communication and leadership.