How Does Garbage Collector Work in .Net c#

While developing an application, a developer creates many objects that occupy memory. An excess amount of unmanaged memory slows down the application performance. Traditional programming languages do not provide garbage collection features such as C language. Although C language ideally suits embedded systems because of its low-level control portability and structured programming. So, working with such kinds of programming languages end up with much effort of freeing up memory manually.

Dot net provides its native garbage collection feature and the developer no longer has to explicitly free memory. The Dot net garbage collector automatically releases the memory when a block of memory is no longer need in the program. This technique prevents memory leaks. In this article, I will explain how to collect garbage memory or objects in Dot net c#. So let’s start.

What is a garbage collector and how does it work?

The garbage collector is one of the main features provided by CLR that helps us to clean unused managed objects. By cleaning unused managed objects, it basically reclaims the memory. When a dot net application runs it can create several objects and at a given moment of time. It is very much possible that it will not use some of those objects or it will not need some of those objects.

What is a garbage collector and how does it work?

So basically for those objects, the garbage collector runs as a background thread continuously at a specific interval. It reclaims the memory once it finds an unused object. So, the garbage collector is nothing but a background thread that runs continuously. Checks for unused managed objects clean those objects and reclaims the memory.

Now, it is important to note that the garbage collector cleans and reclaims unused managed objects only. It does not clean unmanaged objects. So, in other words, anything which is outside the CLR boundary garbage collector will not clean the memory. There is one more concept in garbage collectors that helps in collecting garbage – generations.

Basically, there are three generations that are generation 0, generation 1, and generation 2. Let’s try to understand what is the concept of generations and how does it affect garbage collector performance.

Generations in the garbage collector

Generations in garbage collector - Generation 0, Generation 1, and Generation 2

Basically, there are three generations that are Generation 0, Generation 1, and Generation 2 let’s talk about its key features.

  • Generation 0: This is the youngest generation. All the objects will keep in this generation when an application will create objects. Basically, this generation contains short-lived objects and object collection is very frequent in this generation. Objects that survive this generation promote to the next generation that is Generation ‘1’. 
  • Generation 1: This generation contains objects those were not claim in Generation 0. Garbage collector clears most of the memories and objects in Generation 0 itself but few still having references moves to the next generation.
  •  Generation 2: This generation contains those objects those were not claim in Generation 1 and promoted to this generation. Basically, this generation contains long-lived objects

Let’s understand this below given example.

Short-lived objects vs mid-lived objects vs long-lived objects - comparison

In the above example, you can see when the application executes then all the initialized variables are in generation zero. When periodically the garbage collector finds an object that is not referenced in the program anymore, then it cleans that object and reclaims its memory, and promotes the rest of the object to the next generation. Remember, at the same time it checks for unused objects in generation one and generation two also, but at this point in time, it will not find objects in both generations.

Again in the next periodic cycle, it searches for unused memory in generation zero and reclaims if finds any unused objects and promotes to the next generation. Similarly, this process is repeated for generation one also and seeks for unused objects and reclaims its memory and promotes to next generation.

How to collect garbage objects

You don’t need to do anything If your application creates managed objects. You will have to collect garbage objects explicitly if your application references unmanaged objects by using their native file handles. There are two methods for releasing unmanaged resources Dispose and Finalize.

The Dispose() method

This is just like any other method in the class and can be called explicitly. However, the main role of this method is to clean up objects. In the dispose method, we write clean-up code for the object. It is important that we free up all the unmanaged resources in the dispose() method like database connection, files, etc.

The Finalize() method

Finalize() is called by the Garbage Collector implicitly to free unmanaged resources. The garbage collector calls this method at some point after there are no longer valid references to the object. You must implement this when you are writing a custom class that will be used by other users.

Code example-


using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;

public abstract class Base : IDisposable
{
    private bool disposed = false;
    private string instanceName;
    private List<object> trackingList;

    public Base(string instanceName, List<object> tracking)
    {
        this.instanceName = instanceName;
         trackingList = tracking;
         trackingList.Add(this);
    }

    public string InstanceName
    {
        get
        {
            return instanceName;
        }
    }

    //Implementing IDisposable.
    public void Dispose()
    {
        Console.WriteLine("\n[{0}].Base.Dispose()", instanceName);
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // Freeing other (managed objects).
                Console.WriteLine("[{0}].Base.Dispose(true)", instanceName);
                trackingList.Remove(this);
                Console.WriteLine("[{0}] Removed from tracking list: {1:x16}",
                    instanceName, this.GetHashCode());
            }
            else
            {
                Console.WriteLine("[{0}].Base.Dispose(false)", instanceName);
            }
            disposed = true;
        }
    }

    // Using C# destructor syntax for finalization code.
    ~Base()
    {
        Console.WriteLine("\n[{0}].Base.Finalize()", instanceName);
        Dispose(false);
    }
}

public class Derived : Base
{
    private bool disposed = false;
    private IntPtr umResource;

    public Derived(string instanceName, List<object> tracking) :base(instanceName, tracking)
    {
         // Saving the instance names as an unmanaged resources
         umResource = Marshal.StringToCoTaskMemAuto(instanceName);
    }

    protected override void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                Console.WriteLine("[{0}].Derived.Dispose(true)", InstanceName);
                // Release managed resources.
            }
            else
            {
                Console.WriteLine("[{0}].Derived.Dispose(false)", InstanceName);
            }
            // Release unmanaged resources.
            if (umResource != IntPtr.Zero)
            {
                Marshal.FreeCoTaskMem(umResource);
                Console.WriteLine("[{0}] Unmanaged memory freed at {1:x16}",
                    InstanceName, umResource.ToInt64());
                umResource = IntPtr.Zero;
            }
            disposed = true;
        }
        // Calling Dispose in the base class.
        base.Dispose(disposing);
    }
}

public class TestDisposal
{
    public static void Main()
    {
        List<object> tracking = new List<object>();

        // Dispose is not yet called, Finalize method will be called later on.
        using (null)
        {
            Console.WriteLine("\nDisposal Scenario: #1\n");
            Derived d3 = new Derived("d1", tracking);
        }

        // Dispose method is implicitly called in the scope of the using statement.
        using (Derived d1 = new Derived("d2", tracking))
        {
            Console.WriteLine("\nDisposal Scenario: #2\n");
        }

        // Dispose method is explicitly called.
        using (null)
        {
            Console.WriteLine("\nDisposal Scenario: #3\n");
            Derived d2 = new Derived("d3", tracking);
            d2.Dispose();
        }

        // Again, Dispose method is not called yet, Finalize will be called later on.
        using (null)
        {
            Console.WriteLine("\nDisposal Scenario: #4\n");
            Derived d4 = new Derived("d4", tracking);
        }

        // Object still remaining to dispose.
        Console.WriteLine("\nObjects remaining to dispose = {0:d}", tracking.Count);
        foreach (Derived dd in tracking)
        {
            Console.WriteLine("    Reference Object: {0:s}, {1:x16}",
                dd.InstanceName, dd.GetHashCode());
        }

        // Queued finalizers will be executed when Main() goes out of scope.
        Console.WriteLine("\nDequeueing finalizers...");
    }
}

Concluding words

I hope this article helped you to understand Garbage collection, its advantages, and how the garbage collector works. In my next post, I will describe more about garbage collectors and the difference between Dispose and Finalize.

Leave a Comment