Skip to main content

Command Palette

Search for a command to run...

Garbage Collector (GC)

Discover the Hidden Champion of Memory Management in DOT NET

Updated
4 min read
Garbage Collector (GC)
M

I am a full-stack .NET & React developer. Till now, I have over 5 years of experience working as a professional. Currently, I am working at Vivasoft Ltd. as a Software engineer.

Definition:

It is a background process that runs undeterministically and cleans unreferenced managed objects from memory.

The above definition has 4 marked sections. Let’s discuss about theme first.

Background: The GC runs in the background in a separate thread.

Undeterministic: It doesn’t run periodically in a regular interval. It runs depending on the application memory, processor, GC algorithm, and system pressure, etc.

Unreferenced: When an object goes out of scope (pops from the stack), it becomes unreferenced, which means it is no longer used anywhere.

Managed object: Managed objects are those that are pure .NET objects, and these objects are controlled by the .NET CLR.

Unmanaged objects:

File handles, COM objects, database connections, and network sockets, etc., are not controlled by the .NET CLR runtime. They are unmanaged objects. GC doesn’t collect unmanaged objects. They need to be cleared explicitly by calling their memory free methods like Release, Dispose, Close, etc.

Garbage collection:

When an object is no longer in use, the GC reclaims the memory and returns it to the operating system. In programming terms, when an object is in heap memory and has no reference in the stack(out of scope), its memory is reclaimed by GC.

GC is only concerned with the managed objects, which refers to the memory occupied by objects in heap. GC doesn’t collect primitive data such as int, double, string, etc.

Generations in GC:

Generations are like buckets; every bucket defines how old the objects are. Managed objects in the heap are grouped into generations based on how long they have been alive.

Gen 0: Short-lived objects, such as local & temporary objects.

Gen 1: Medium-lived objects. Also known as the buffer between GEN0 & GEN2.

Gen 2: Long-lived objects, such as static objects, caches, global objects, singletons, etc.

When GC runs:

  • It usually starts with Gen 0 and tries to clean.

  • Objects that still have a reference are promoted to Gen 1.

  • In a Gen 1 collection, if they survive again, they are moved to Gen 2.

  • Gen 2 collection is less frequent because long-lived objects are used for a long time, and they are less likely to be garbage.

  • Large objects (>= 85 KB) are stored in the large object heap(LOH). They are collected only during Gen 2 collections.

Why need generations?

Gen 0 most frequent and Gen 2 least frequent in the collection. Generation concepts are made for garbage collection to be faster and efficient. Checking long-lived objects every time to see if they are referenced or not is a waste of time.

Let’s handle the unmanaged objects.

We have two options to clean an unmanaged object.

  • Using Destructor / Finalizer

  • Using the dispose pattern

// Let's say this class is using unmanaged resources. Like file handler, database connections etc.
public class ResourceManager : IDsiposable
{
    public ResourceManager() {}

    // Finalize or destructor
    ~ResourceManager() 
    {
        // Clean the unmanage resource here
    }

    public void Dispose() 
    {
        // Clean the unmanage resource here
        GC.SuppressFinalize(this);
    }
}

If we go with the finalizer, the GC will call it before collecting it. This is undeterministic and usually keeps the object alive for a long time.

Using the dispose pattern is a recommended way to clean up unmanaged resources. The developer is responsible for calling the dispose method. Dispose call can be made automatically using a dependency injection framework or using ‘using()‘ syntax. Calling GC.SuppressFinalize() It is optional, but it improves performance. It tells the GC that no need to call the finalizer for this object.

If we register a class as a request scope or a transient lifetime, our DI will automatically call Dispose() for that object.

// Program.cs
builder.Services.AddScoped<ResourceManager>();

We can call the Dispose() method ourselves.

var resourceManager = new ResourceManager();
// Do required work
// call dispoise
resourceManager.Dispose();

We can make the call automatic using the ‘using‘ syntax.

using (var resourceManager = new ResourceManager()) 
{
    // Do required task
}

// New short syntax
using var resourceManager = new ResourceManager();

After the ‘using‘ scope ends, the Dispose() method will be called automatically. This syntax is a syntactic sugar of try-finally.

var resourceManager = new ResourceManager()
try {
    // Do required tasks
}
finally {
    resourceManager?.Dispose();
}

Can we force GC to run?

Yes, we can. By using GC.Collect(int generation) We can also specify which generation to run in as an argument. GC is very smart; we should not force it. It is recommended not to force it.

GC is helpful to find memory issues. We can inspect our project’s memory using Visual Studio’s Performance Profiler, which is under the Debug menu.

References: