In this article, I have explained how garbage collection works in C# with an example of console application, but before we proceed further, let's understand what does garbage collection mean?

When we run a program, we use some memeory of system (CPU or RAM), but what happens to that allocated memory, when our program runs successfully? This is where Garbage collector comes in use, it automatically releases objects and free memory, when system has low physical memory, and hence memory is reclaimed by Operating System to use it in other tasks or programs.

Whats does Garbage means in C#?

"Garbage" consists of objects created during a program’s execution on the managed heap that are no longer accessible by the program.

Their memory can be reclaimed and reused with no averse effects.

Advantage of Garbage Collector

  1. No worry about memory management
  2. Allocate object memory on managed heap efficiently.
  3. Reclaims objects that no longer being used, clears their memory and keeps the memory for future allocations.
  4. Provides memory safety by making sure that object cannot use the content of other object.

Let take a look at working Console application example of how we can call garbarge collector explicitly in .NET C# but we before we proceed for that, generally we know that, we write destructor to destroy the object which is created by constructor, but in .NET destructor not runs automatically.

Let us see how it works.

using System;

namespace GarbageCollection
{
    public class GarbageCheck
    {
        public GarbageCheck()
        {
            Console.WriteLine("Reserve memory");
        }
        ~GarbageCheck()
        {
            Console.WriteLine("Free memory");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            GarbageCheck g = new GarbageCheck();
            g = null;

            Console.ReadLine();
        }
    }

}

Output:

garbage-collection-in-c-sharp-min.png

As you can see in the above code, Destructor wasn't called by it's own.

Now, if we want to free all unused object from memory, we call garbage collector explicitly using GC.collect() method.

Here, GC is garbage collector class. In below example we are removing all unused object from memory using GC.collect() .

using System;

namespace GarbageCollection
{
    public class GarbageCheck
    {
        public GarbageCheck()
        {
            Console.WriteLine("Reserve memory");
        }
        ~GarbageCheck()
        {
            Console.WriteLine("Free memory");
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            GarbageCheck g = new GarbageCheck();
            g = null;

            //free memory by calling Garbage collector
            GC.Collect();

            Console.ReadLine();
        }
    }

}

garbage-collection-in-c-sharp-two-min.png

Note: Calling GC.Collect is discouraged by Microsoft and is generally to be avoided. The garbage collector is a very intelligent mechanism, and has much more information at its disposal than you do when it evaluates whether or not a collection is needed.

How Garbage Collector works

In .NET GC Class, controls the system garbage collector, a service that automatically reclaims unused memory.

Garbage collector initialized by CLR and allocate memory for object, this memory is known as managed heap.

Managed heap organized into form of Generation that contains reference of objects. Garbage Collector divides complete managed heap into three generation as:

Generation 0: This is the youngest generation and contains short-lived objects. An example of a short-lived object is a temporary variable. Garbage collection occurs most frequently in this generation.

Newly allocated objects form a new generation of objects and are implicitly generation 0 collections. However, if they are large objects, they go on the large object heap (LOH), which is sometimes referred to as generation 3. Generation 3 is a physical generation that's logically collected as part of generation 2.

Generation 1: This generation contains short-lived objects and serves as a buffer between short-lived objects and long-lived objects.

After the garbage collector performs a collection of generation 0, it compacts the memory for the reachable objects and promotes them to generation 1. Because objects that survive collections tend to have longer lifetimes, it makes sense to promote them to a higher generation. The garbage collector doesn't have to reexamine the objects in generations 1 and 2 each time it performs a collection of generation 0.

Generation 2: This generation contains long-lived objects. An example of a long-lived object is an object in a server application that contains static data that's live for the duration of the process.

Objects in generation 2 that survive a collection remain in generation 2 until they are determined to be unreachable in a future collection.

Take a look at Generation Console application example

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace GarbageCollectorInCSharp
{
    class GCProgram
    {
        private const long maxGarbage = 1000;

        static void Main()
        {
            GCProgram myGCCol = new GCProgram();

            // Determine the maximum number of generations the system
            // garbage collector currently supports.
            Console.WriteLine("The highest generation is {0}", GC.MaxGeneration);

            myGCCol.MakeSomeGarbage();

            // Determine which generation myGCCol object is stored in.
            Console.WriteLine("Generation: {0}", GC.GetGeneration(myGCCol));

            // Determine the best available approximation of the number
            // of bytes currently allocated in managed memory.
            Console.WriteLine("Total Memory: {0}", GC.GetTotalMemory(false));

            // Perform a collection of generation 0 only.
            GC.Collect(0);

            // Determine which generation myGCCol object is stored in.
            Console.WriteLine("Generation: {0}", GC.GetGeneration(myGCCol));

            Console.WriteLine("Total Memory: {0}", GC.GetTotalMemory(false));

            // Perform a collection of all generations up to and including 2.
            GC.Collect(2);

            // Determine which generation myGCCol object is stored in.
            Console.WriteLine("Generation: {0}", GC.GetGeneration(myGCCol));
            Console.WriteLine("Total Memory: {0}", GC.GetTotalMemory(false));
            Console.Read();
        }

        void MakeSomeGarbage()
        {
            Version vt;

            for (int i = 0; i < maxGarbage; i++)
            {
                // Create objects and release them to fill up memory
                // with unused objects.
                vt = new Version();
            }
        }
    }
}

Output:

The highest generation is 2
Generation: 0
Total Memory: 62756
Generation: 1
Total Memory: 38372
Generation: 2
Total Memory: 38296

How GC works internally?

Process first

Whenever new object is created, it comes into generation 0.

When Generation 0 is completely filled and application is trying to create a new object then:

  1. Garbage collector perform an operation known as collection means.
  2. Garbage collector examines all the object that present in generation 0, identifies idled objects and objects in use.
  3. Idled objects are destroyed or placed in finalization queue.
  4. Objects in use that survive in generation 0 and promoted to next generation i.e. generation 1 so that generation 0 will become empty.
  5. Now again newly created object place inside generation 0.
  6. After some performing collection generation 0 and generation 1 will be completely filled.

Process second

When generation 0 and generation 1 completely filled and application is trying to create a new object then:

  1. Garbage collection again perform the operation collection means.
  2. Examines all the object present in generation 0 and generation 1, identifies idled objects and objects in use.
  3. Idled objects are destroyed or placed in finalization queue.
  4. Objects in use that survive in generation 1 and promoted to the next generation i.e generation 2, so that generation 0 and generation 1 will become empty.
  5. Now again newly created object will be placed in generation 0.
  6. After some performing so many collections generation 0, generation 1 and generation 2 will be completely filled.

Process third

When generation 0, 1, 2 are completely filled and application is trying to created a new object then:

  1. Garbage collector again perform the operation collection means.
  2. Examine all the objects in generation 0, generation 1 and generation 2.
  3. Divided object into idled objects and objects in use.
  4. Idled objects are destroyed or place in Finalization queue.
  5. Objects in use that survive in generation 2 remain in generation 2.
  6. Now newly created object placed inside generation 0.
  7. Generation 2 is also known as full garbage collection, because it reclaims all objects from all generations.

Performing the collection of generation 0 garbage collector will take 1/10th of a nano second time which is less than a page cycle.

How garbage collector identify that objects in use or not

Garbage collector use following information to determine whether objects are in use or not:

  1. Stack roots - Stack variables provided by JIT compiler and stack walker.
  2. Garbage collection handler - Handles that points to managed object and can be allocated by user code or by CLR.
  3. Stack data - Static object in application domain that could be referencing other objects. Each application domain keep track of its static objects.

Before a garbage collection starts, all managed threads are suspended except for the thread that triggered the garbage collection.

The garbage collector tracks and reclaims objects allocated in managed memory. Periodically, the garbage collector performs garbage collection to reclaim memory allocated to objects for which there are no valid references.

Garbage collection happens automatically when a request for memory cannot be satisfied using available free memory.

Alternatively, an application can force garbage collection using the Collect method.

Complete Garbage Collection Notifications Example

In the following example, a group of servers service incoming Web requests. To simulate the workload of processing requests, byte arrays are added to a List<T> collection. Each server registers for a garbage collection notification and then starts a thread on the WaitForFullGCProc user method to continuously monitor the GCNotificationStatus enumeration that is returned by the WaitForFullGCApproach and the WaitForFullGCComplete methods.

The WaitForFullGCApproach and the WaitForFullGCComplete methods call their respective event-handling user methods when a notification is raised:

OnFullGCApproachNotify: This method calls the RedirectRequests user method, which instructs the request queuing server to suspend sending requests to the server. This is simulated by setting the class-level variable bAllocate to false so that no more objects are allocated.

Next, the FinishExistingRequests user method is called to finish processing the pending server requests. This is simulated by clearing the List<T> collection.

Finally, a garbage collection is induced because the workload is light.

OnFullGCCompleteNotify: This method calls the user method AcceptRequests to resume accepting requests because the server is no longer susceptible to a full garbage collection. This action is simulated by setting the bAllocate variable to true so that objects can resume being added to the List<T> collection.

C# Console application code

using System;
using System.Collections.Generic;
using System.Threading;

namespace GCNotify
{
    class Program
    {
        // Variable for continual checking in the
        // While loop in the WaitForFullGCProc method.
        static bool checkForNotify = false;

        // Variable for suspending work
        // (such servicing allocated server requests)
        // after a notification is received and then
        // resuming allocation after inducing a garbage collection.
        static bool bAllocate = false;

        // Variable for ending the example.
        static bool finalExit = false;

        // Collection for objects that
        // simulate the server request workload.
        static List<byte[]> load = new List<byte[]>();

        public static void Main(string[] args)
        {
            try
            {
                // Register for a notification.
                GC.RegisterForFullGCNotification(10, 10);
                Console.WriteLine("Registered for GC notification.");

                checkForNotify = true;
                bAllocate = true;

                // Start a thread using WaitForFullGCProc.
                Thread thWaitForFullGC = new Thread(new ThreadStart(WaitForFullGCProc));
                thWaitForFullGC.Start();

                // While the thread is checking for notifications in
                // WaitForFullGCProc, create objects to simulate a server workload.
                try
                {

                    int lastCollCount = 0;
                    int newCollCount = 0;

                    while (true)
                    {
                        if (bAllocate)
                        {
                            load.Add(new byte[1000]);
                            newCollCount = GC.CollectionCount(2);
                            if (newCollCount != lastCollCount)
                            {
                                // Show collection count when it increases:
                                Console.WriteLine("Gen 2 collection count: {0}", GC.CollectionCount(2).ToString());
                                lastCollCount = newCollCount;
                            }

                            // For ending the example (arbitrary).
                            if (newCollCount == 500)
                            {
                                finalExit = true;
                                checkForNotify = false;
                                break;
                            }
                        }
                    }
                }
                catch (OutOfMemoryException)
                {
                    Console.WriteLine("Out of memory.");
                }

                finalExit = true;
                checkForNotify = false;
                GC.CancelFullGCNotification();
            }
            catch (InvalidOperationException invalidOp)
            {

                Console.WriteLine("GC Notifications are not supported while concurrent GC is enabled.\n"
                    + invalidOp.Message);
            }
        }

        public static void OnFullGCApproachNotify()
        {

            Console.WriteLine("Redirecting requests.");

            // Method that tells the request queuing
            // server to not direct requests to this server.
            RedirectRequests();

            // Method that provides time to
            // finish processing pending requests.
            FinishExistingRequests();

            // This is a good time to induce a GC collection
            // because the runtime will induce a full GC soon.
            // To be very careful, you can check precede with a
            // check of the GC.GCCollectionCount to make sure
            // a full GC did not already occur since last notified.
            GC.Collect();
            Console.WriteLine("Induced a collection.");
        }

        public static void OnFullGCCompleteEndNotify()
        {
            // Method that informs the request queuing server
            // that this server is ready to accept requests again.
            AcceptRequests();
            Console.WriteLine("Accepting requests again.");
        }

        public static void WaitForFullGCProc()
        {
            while (true)
            {
                // CheckForNotify is set to true and false in Main.
                while (checkForNotify)
                {
                    // Check for a notification of an approaching collection.
                    GCNotificationStatus s = GC.WaitForFullGCApproach();
                    if (s == GCNotificationStatus.Succeeded)
                    {
                        Console.WriteLine("GC Notification raised.");
                        OnFullGCApproachNotify();
                    }
                    else if (s == GCNotificationStatus.Canceled)
                    {
                        Console.WriteLine("GC Notification cancelled.");
                        break;
                    }
                    else
                    {
                        // This can occur if a timeout period
                        // is specified for WaitForFullGCApproach(Timeout)
                        // or WaitForFullGCComplete(Timeout)
                        // and the time out period has elapsed.
                        Console.WriteLine("GC Notification not applicable.");
                        break;
                    }

                    // Check for a notification of a completed collection.
                    GCNotificationStatus status = GC.WaitForFullGCComplete();
                    if (status == GCNotificationStatus.Succeeded)
                    {
                        Console.WriteLine("GC Notification raised.");
                        OnFullGCCompleteEndNotify();
                    }
                    else if (status == GCNotificationStatus.Canceled)
                    {
                        Console.WriteLine("GC Notification cancelled.");
                        break;
                    }
                    else
                    {
                        // Could be a time out.
                        Console.WriteLine("GC Notification not applicable.");
                        break;
                    }
                }

                Thread.Sleep(500);
                // FinalExit is set to true right before
                // the main thread cancelled notification.
                if (finalExit)
                {
                    break;
                }
            }
        }

        private static void RedirectRequests()
        {
            // Code that sends requests
            // to other servers.

            // Suspend work.
            bAllocate = false;
        }

        private static void FinishExistingRequests()
        {
            // Code that waits a period of time
            // for pending requests to finish.

            // Clear the simulated workload.
            load.Clear();
        }

        private static void AcceptRequests()
        {
            // Code that resumes processing
            // requests on this server.

            // Resume work.
            bAllocate = true;
        }
    }
}

Reference: https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/notifications

Output:

garbage-collector-notifications-sample.gif