Garbage Collection in C#: Managing Memory Efficiently

Garbage Collection in CSharp

What is Garbage Collection in C#?

In simple terms, In the Common Language Runtime (CLR), there’s a helpful feature called the garbage collector (GC). This GC takes care of memory for your applications automatically.

So, if you are a developer working with managed code, you don’t need to write extra lines of code to manage memory. This automatic memory management prevents issues like forgetting to free up memory, which can lead to memory leaks or trying to use memory that’s already been freed.

So, you don’t need to worry about accidentally leaving a mess behind, which could cause your program to slow down or even crash.

The garbage collector keeps track of which objects are no longer used by the application and automatically reclaims their memory. Thus, CG makes life easier for developers by automatically handling memory cleanup, so you can focus on writing awesome code without the hassle of memory management.

Garbage collection is a process in C# that automatically manages memory by reclaiming memory occupied by objects that are no longer needed in the program. 

Benefits of garbage collection in C#

Here are the benefits of garbage collection in C#:

  • No Manual Memory Cleanup: Garbage collection frees you from the chore of manually cleaning up memory while developing your application. You don’t have to worry about forgetting to release memory when you’re done with it.
  • Memory Management: When objects are no longer used, GC will reclaim those objects by clearing their memory, and keep their memory available for future allocations.
  • Generational Collection: GC categorizes objects into generations for efficient memory cleanup (for example, Generation 0, 1, and 2).
  • Fewer Memory Leaks: Memory leaks, which can slow down or crash your program, become less likely because the garbage collector automatically identifies and reclaims unused memory.
  • Better Code Quality: With garbage collection, you can focus on writing code for your application’s logic rather than dealing with memory management, leading to more efficient and reliable programs.
  • Optimized Performance: Garbage collection is designed to run efficiently and ensure that your application runs smoothly even as it manages memory in the background.

How Does Garbage Collection Work?

Garbage collection operates by identifying and freeing up memory that is no longer accessible by the application. 

It follows these essential steps, phases and memory generations:

Garbage Collection Different Phases

Phase 1: Mark

In the Mark phase, the GC traverses the object graph starting from root objects (e.g., global variables, stack frames, and CPU registers) and marks all reachable objects as alive. Any object that is not reachable is left unmarked and is considered for cleanup from the heap memory.

Phase 2: Sweep

The sweep phase is responsible for reclaiming memory occupied by objects that are not marked as alive. It effectively frees up memory that can be safely reused in the future.

Phase 3: Finalization (optional) – Running Finalizers

  • If objects have finalizers (destructors), the Finalization phase runs these finalizers for the objects that are about to be collected.
  • Finalizers allow objects to perform cleanup operations before they are removed from memory.
  • This phase is optional and only applies to objects with finalizers.

Phase 4: Compact (optional)

During the compacting phase of garbage collection, the focus is on rearranging live objects in the managed heap to reduce memory fragmentation.

Live objects that survive the collection process are moved toward the younger end of the heap memory, ensuring that memory remains efficiently utilized. Dead objects are not physically moved but are marked as free space, making their memory available for future allocations.

Phase 5: Promotion (Generational Collection) – Moving Objects

  • In generational garbage collection, objects that survive collections in lower generations (e.g., Generation 0 to Generation 1) get promoted to higher generations (e.g., Generation 1 to Generation 2).
  • This phase involves moving objects between generations based on their longevity.
  • Objects in higher generations are subject to less frequent collection.
Phase-In-Garbage-Collection-in-CSharp

Garbage Collection Heap Generation

The .NET Framework maintains memory in three separate generations like generation 0, generation 1, and generation 2, so that it can handle long-lived and short-lived objects separately:

Generation 0:

  • Purpose: Generation 0 is the youngest generation and is designed for short-lived objects. It serves as the initial placement for newly created objects.
  • Collection Frequency: Garbage collection for Generation 0 occurs frequently because most objects in this generation are short-lived (such as temporary variables) and tend to become garbage collected when they are no longer used in the application..
  • Promotion: If an object survives a garbage collection cycle in Generation 0, it gets promoted to Generation 1.
  • Memory Allocation: Objects are initially allocated memory in Generation 0 when they are created using the new keyword.

The new objects are initially put in Generation 0 because, in most cases, objects have short lifetimes. By frequently collecting Generation 0, the garbage collector can quickly identify and free up memory used by objects that are no longer in use.

Generation 1:

  • Purpose: Generation 1 contains objects that have survived one or more garbage collection cycles in Generation 0. These objects are considered to have a longer lifetime than those in Generation 0.
  • Collection Frequency: Garbage collection for Generation 1 happens less frequently compared to Generation 0 but more frequently than Generation 2.
  • Promotion: If an object survives a garbage collection cycle in Generation 1, it will promoted to Generation 2.
  • Memory Allocation: Objects can be allocated memory in Generation 1 if they are promoted from Generation 0.

Objects that manage to survive the collection process in Generation 0 move up to Generation 1. Generation 1 is like a middle ground for objects. It holds onto objects that are not short-lived but not long-lived either.

If cleaning up Generation 0 doesn’t free up enough memory for new objects, the garbage collector will go on to clean up Generation 1 and then Generation 2. Any objects in Generation 1 that manage to survive these cleanups get promoted to Generation 2.

Generation 2:

  • Purpose: Generation 2 is reserved for long-lived objects that have survived multiple garbage collection cycles in both Generation 0 and Generation 1. These objects are expected to survive for a long duration.
  • Collection Frequency: Garbage collection for Generation 2 occurs the least frequently because long-lived objects are less likely to become garbage.
  • Promotion: Objects in Generation 2 tend to stay there unless they are explicitly collected or the application terminates.
  • Memory Allocation: Objects can be allocated memory in Generation 2 if they are promoted from lower generations.

Generation 2 is the last stop for memory management, which contains long-lived objects that might sometimes stay throughout the application’s lifespan. 

When an object from Generation 1 successfully survives another garbage collection, it gets promoted to Generation 2. Objects in Generation 2 typically consume more memory and tend to be longer-lived compared to objects in earlier generations. However, their lifespan may vary depending on the application’s usage and memory management requirements.

Understanding GC Generations using Code Example

This code helps you understand how objects move between generations during garbage collection in C#.

using System;

class Program
{
    static void Main()
    {
        // Create objects in Generation 0
        var obj1 = new SomeClass();
        var obj2 = new SomeClass();

        Console.WriteLine($"Generation of obj1: {GC.GetGeneration(obj1)}");
        Console.WriteLine($"Generation of obj2: {GC.GetGeneration(obj2)}");

        // Trigger a collection of Generation 0
        GC.Collect(0);

        Console.WriteLine("After collecting Generation 0");

        Console.WriteLine($"Generation of obj1: {GC.GetGeneration(obj1)}");
        Console.WriteLine($"Generation of obj2: {GC.GetGeneration(obj2)}");

        // Promote obj1 to Generation 1
        var obj3 = new SomeClass();

        Console.WriteLine($"Generation of obj1: {GC.GetGeneration(obj1)}");
        Console.WriteLine($"Generation of obj2: {GC.GetGeneration(obj2)}");
        Console.WriteLine($"Generation of obj3: {GC.GetGeneration(obj3)}");

        // Trigger a collection of Generation 0 and 1
        GC.Collect(0);
        GC.Collect(1);

        Console.WriteLine("After collecting Generation 0 and 1");

        Console.WriteLine($"Generation of obj1: {GC.GetGeneration(obj1)}");
        Console.WriteLine($"Generation of obj2: {GC.GetGeneration(obj2)}");
        Console.WriteLine($"Generation of obj3: {GC.GetGeneration(obj3)}");
    }
}

class SomeClass
{
    ~SomeClass()
    {
        Console.WriteLine("Finalizing SomeClass");
    }
}

Output:

Generation of obj1: 0
Generation of obj2: 0
After collecting Generation 0
Generation of obj1: 1
Generation of obj2: 1
Generation of obj1: 1
Generation of obj2: 1
Generation of obj3: 0
After collecting Generation 0 and 1
Generation of obj1: 2
Generation of obj2: 2
Generation of obj3: 2
Finalizing SomeClass
Finalizing SomeClass
Finalizing SomeClass

Code Explanation:

In this code:

  • We create objects obj1 and obj2 in Generation 0.
  • We use GC.GetGeneration(obj) to determine the generation of each object.
  • We trigger a collection of Generation 0 using GC.Collect(0) and observe the changes in object generations.
  • We create obj3, which is placed in Generation 0 by default.
  • We again check the generations of all objects.
  • Finally, we trigger a collection of both Generation 0 and Generation 1 using GC.Collect(0) and GC.Collect(1) and observe the final generations of the objects.

When does garbage collection in C# get triggered?

Garbage collection in C# occurs automatically and at specific times during the execution of your program. The .NET Framework manages the timing of GC. Here are the key moments when GC typically happens:

  1. When Memory Pressure Rises: The garbage collector monitors the memory usage of your program. The garbage collector is triggered when the available memory becomes insufficient or there is “memory pressure” (i.e., not enough free memory for new allocations).
  2. During Allocation: When you allocate memory for a new object using the new keyword, the garbage collector checks if there’s enough free memory. If not, it may initiate a collection cycle to free up space before allowing the allocation.
  3. When You Explicitly Request It: You can explicitly trigger garbage collection using the GC.Collect() method. However, it’s generally discouraged to do this, as the garbage collector is already designed to run when needed. Overusing GC.Collect() can lead to unnecessary performance overhead.
  4. Generation Thresholds: The garbage collector operates in multiple generations (Generation 0, Generation 1, and Generation 2). Collections occur more frequently in Generation 0, less frequently in Generation 1, and even less frequently in Generation 2. When a generation fills up, GC is triggered for that specific generation.
  5. Finalization: If your program has objects with finalizers (objects that need cleanup when they are no longer required), the garbage collector will perform a special collection cycle to run finalizers before reclaiming the memory. This is often referred to as a “garbage collection with finalization.”

Garbage Collection Code Examples

Let’s explore a couple of important methods provided by the System.GC class:

GC.GetTotalMemory()

The GC.GetTotalMemory() method returns the total number of bytes allocated in the managed heap. It can be useful for monitoring memory usage in your application.

using System;

class Program
{
    static void Main()
    {
        // Get the total memory allocated in bytes
        long totalMemory = GC.GetTotalMemory(false); // Pass 'false' for less accurate but faster result

        Console.WriteLine($"Total Memory: {totalMemory} bytes");
    }
}
// Output: Total Memory: 30292 bytes

In this example, we use GC.GetTotalMemory() to retrieve the total memory allocated in bytes.

GC.GetGeneration(object obj)

The GC.GetGeneration(object obj) method allows you to determine the generation in which a specific object resides. This can be useful for understanding the lifetime of objects and optimizing memory management.

using System;

class Program
{
    static void Main()
    {
        // Create an object
        var obj = new SomeClass();

        // Determine the generation of the object
        int generation = GC.GetGeneration(obj);

        Console.WriteLine($"The object is in Generation {generation}.");
    }
}

class SomeClass
{
    // destructor
    ~SomeClass()
    {
        Console.WriteLine("Finalizing SomeClass");
    }
}

Output:

The object is in Generation 0.
Finalizing SomeClass

GC.Collect() Method

The GC.Collect() method in C# is used to explicitly trigger garbage collection. However, it’s generally not recommended to use this method regularly, as the garbage collector is designed to work automatically and efficiently manage memory. 

Explicitly calling GC.Collect() can lead to unnecessary performance overhead.

Here is a C# code example to demonstrate the GC.Collect() method:

using System;

class Program
{
    static void Main()
    {
        // Create class objects
        var obj1 = new MyClass();
        var obj2 = new MyClass();

        // Display memory statistics before garbage collection
        Console.WriteLine($"Total Memory Before GC: {GC.GetTotalMemory(false)} bytes");

        // Explicitly trigger garbage collection for Generation 0
        GC.Collect(0);

        // Display memory statistics after garbage collection
        Console.WriteLine($"Total Memory After GC: {GC.GetTotalMemory(false)} bytes");

        // Note: Explicitly calling GC.Collect() is typically not recommended in real-world applications.
    }
}

class MyClass
{
    ~MyClass()
    {
        Console.WriteLine("Finalizing MyClass");
    }
}

//Output:
// Total Memory Before GC: 30292 bytes
// Total Memory After GC: 30020 bytes

GC.KeepAlive()

The GC.KeepAlive() method prevents an object from being prematurely collected by the garbage collector. It’s particularly useful when you want to ensure that an object stays alive for a specific duration, even if it’s not actively referenced in your code.

using System;

class Program
{
    static void Main()
    {
        // Create an object
        var obj = new SomeClass();

        // Use GC.KeepAlive to prevent premature collection
        GC.KeepAlive(obj);

        // obj can still be used here
        obj.SomeMethod();
    }
}

class SomeClass
{
    public void SomeMethod()
    {
        Console.WriteLine("Calling SomeMethod...");
    }

    ~SomeClass()
    {
        Console.WriteLine("Finalizing SomeClass");
    }
}

FAQs

Here, we have some frequently asked questions (FAQs) related to GC.

Q1: What is garbage collection in C#?

Garbage collection in C# is an automatic memory management process that identifies and reclaims memory occupied by objects that are no longer in use. It helps prevent memory leaks and enhances program stability.

Q2: How does garbage collection work in C#?

Garbage collection in C# works by dividing the managed heap into generations (Generation 0, Generation 1, and Generation 2) and employing algorithms like the generational and mark-and-sweep algorithms to identify and clean up unused objects.

Q3: When does garbage collection occur in C#?

Garbage collection in C# occurs automatically when memory pressure rises, during new object allocation, and when specific generation thresholds are reached. It also can be triggered explicitly using GC.Collect(), but this is generally discouraged.

Q4: What is the difference between the garbage collector and the Dispose() method?

The key difference between the garbage collector and the Dispose() method in C# is:
1. Garbage Collector automatically manages memory by identifying and reclaiming unused objects. It’s non-deterministic and runs in the background. Developers don’t need to explicitly release memory.

2. Dispose Method: Allows developers to manually release unmanaged resources (e.g., file handles or database connections) when they are no longer needed. It provides more control but requires explicit implementation using IDisposable Interface.
The garbage collector focuses on memory management, while Dispose is used for resource cleanup.

Q5: What is the disadvantage of garbage collection?

The main disadvantage of garbage collection is Non-deterministic Timing.
Garbage collection occurs at non-deterministic times, meaning developers have limited control over when memory is reclaimed. This lack of control can be a drawback for real-time or low-latency applications.

References: MSDN- Fundamentals of garbage collection

Recommended Articles:

Shekh Ali
5 1 vote
Article Rating
Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments