Garbage Collection in .NET Core

Garbage collection (GC) is an automatic memory management feature in .NET Core. It reclaims memory occupied by objects that are no longer in use by the application, preventing memory leaks and simplifying development.

How Garbage Collection Works

The .NET GC operates on a generational model. It divides the managed heap into generations (0, 1, and 2) and a large object heap (LOH). Newly created objects are placed in Generation 0. When Generation 0 becomes full, a GC occurs, and any objects that survive are promoted to Generation 1. This process continues, with objects surviving multiple collections being promoted to higher generations.

The GC identifies unreachable objects by starting from a set of root objects (e.g., static fields, thread stacks, global variables) and traversing the object graph. Any object that cannot be reached from a root is considered garbage.

Generations

  • Generation 0: Contains the youngest objects. This generation is collected most frequently.
  • Generation 1: Contains objects that survived a Generation 0 collection.
  • Generation 2: Contains objects that survived a Generation 1 collection. This generation is collected least frequently, as it typically holds long-lived objects.
  • Large Object Heap (LOH): Used for allocating large objects (typically > 85,000 bytes). LOH collection is less frequent and can be more performance-intensive.

This generational approach optimizes GC performance by focusing collection efforts on younger, more transient objects, which are more likely to be garbage.

GC Triggers

The GC can be triggered by several events:

  • Memory Pressure: When the system experiences low memory conditions.
  • Object Allocation: When the managed heap runs out of space for new object allocations.
  • Explicit Calls: Developers can explicitly request a GC using GC.Collect(), though this is generally discouraged as it can lead to performance issues.

GC Modes

.NET Core supports two primary GC modes:

  • Workstation GC: Optimized for client applications, aiming for low latency and responsiveness. It typically runs in a single background thread.
  • Server GC: Optimized for server applications, aiming for high throughput by using multiple GC threads. It's the default for server applications.

The mode can be configured via the runtimeconfig.json file or environment variables.

Best Practices

  • Avoid explicit GC.Collect(): Let the runtime manage memory automatically.
  • Dispose of unmanaged resources: Use the IDisposable interface and the using statement to ensure unmanaged resources are released promptly.
  • Be mindful of large objects: Frequent allocation and deallocation of very large objects can impact LOH performance.
  • Understand object lifetimes: Keep the scope of objects as short as possible.

Tip: While the GC handles most memory management, understanding object lifecycles and properly managing unmanaged resources remains crucial for building efficient and stable .NET applications.

Code Example (Conceptual)

Here's a simplified look at how objects are created and how the GC reclaims them:

C# Example

using System;

public class MyObject
{
    public int Id { get; set; }
    public byte[] Data { get; set; }

    public MyObject(int id, int size)
    {
        Id = id;
        Data = new byte[size];
        Console.WriteLine($"MyObject {Id} created.");
    }

    ~MyObject()
    {
        Console.WriteLine($"MyObject {Id} is being finalized (garbage collected).");
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        Console.WriteLine("Starting object creation...");

        // Create some objects
        for (int i = 0; i < 5; i++)
        {
            MyObject obj = new MyObject(i, 1024);
            // 'obj' goes out of scope here, potentially making it eligible for GC
        }

        Console.WriteLine("\nExplicitly triggering GC (for demonstration only)...");
        GC.Collect(); // In production, avoid this!
        GC.WaitForPendingFinalizers(); // Wait for finalizers to complete

        Console.WriteLine("\nProgram finished.");
    }
}

In this example, objects created within the loop become eligible for garbage collection when the loop iteration ends and the variable holding the reference goes out of scope. The finalizer (~MyObject()) is called when the GC reclaims the object's memory.