Skip to main content

🗑️ Garbage Collection & Resource Management in .NET (C#) — Beginner Friendly Guide

When you start working with .NET and C#, one of the biggest advantages is that you don’t need to manually manage memory like in C or C++. The Garbage Collector (GC) does most of the work for you.

But here’s the catch — not everything is managed automatically. Some resources like files, database connections, sockets, and native memory still need special handling.

This blog will help you understand:
✔ How the GC works
✔ What are managed vs unmanaged resources
✔ The difference between Dispose, Finalize, and using
✔ The Dispose pattern with examples
✔ Best practices every C# developer should know


1) How Garbage Collection Works in .NET

  • Managed resources → Normal .NET objects (string, List, etc.). GC frees them automatically.
  • Unmanaged resources → External resources like file handles, database connections, sockets, native memory. GC cannot clean them up — you must do it.

👉 GC uses a Generational Model for performance:

  • Gen 0: Short-lived objects (local variables, temp strings).
  • Gen 1: Objects that survived Gen 0.
  • Gen 2: Long-lived objects (static data, cached objects).

GC runs automatically when memory pressure increases, not immediately after an object is unused.


2) Managed vs Unmanaged Resources

Managed resources → handled by GC.
Unmanaged resources → require explicit release (e.g., OS file handles).

Example:

  • string, List<int> → Managed
  • FileStream, SqlConnection, IntPtr (native pointer) → Wrap unmanaged resources → Need Dispose()

3) Dispose vs Finalize vs Using

Feature Dispose (IDisposable) Finalize (~ClassName) using
Who calls it? Developer GC Compiler (auto)
When runs? Deterministic (immediate) Non-deterministic (GC decides) At end of scope
Use case Release managed + unmanaged resources Safety net for unmanaged cleanup Ensure Dispose is called safely

4) Example: Simple IDisposable

If your class only holds managed disposable resources:

public class FileProcessor : IDisposable
{
    private readonly FileStream _file;

    public FileProcessor(string path)
    {
        _file = new FileStream(path, FileMode.Open);
    }

    // Dispose method
    public void Dispose()
    {
        // Clean up managed IDisposable resources
        _file?.Dispose();
    }
}

// ✅ Usage with "using"
using (var processor = new FileProcessor("data.txt"))
{
    // work with processor
} // Dispose() is called automatically here

5) Example: Full Dispose Pattern (Managed + Unmanaged)

If your class holds unmanaged resources (like native memory):

using System;
using System.Runtime.InteropServices;

public class NativeWrapper : IDisposable
{
    private IntPtr _nativeResource = IntPtr.Zero; // unmanaged memory
    private bool _disposed = false;

    public NativeWrapper()
    {
        // Allocate unmanaged memory
        _nativeResource = Marshal.AllocHGlobal(100);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this); // Avoid finalizer overhead
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;

        if (disposing)
        {
            // Dispose managed resources here (if any)
        }

        // Free unmanaged resource
        if (_nativeResource != IntPtr.Zero)
        {
            Marshal.FreeHGlobal(_nativeResource);
            _nativeResource = IntPtr.Zero;
        }

        _disposed = true;
    }

    // Finalizer - acts as safety net
    ~NativeWrapper()
    {
        Dispose(false);
    }
}

6) Using using for Cleanup

Instead of calling Dispose() manually:

// Classic using block
using (var stream = new FileStream("file.txt", FileMode.Open))
{
    // use stream
} // Dispose() called automatically

C# 8+ shorthand:

using var stream = new FileStream("file.txt", FileMode.Open);
// Dispose() auto-called at end of scope

7) SafeHandle — Better than Finalizers

Instead of writing your own finalizer, use SafeHandle (built-in) which properly wraps OS handles.

using Microsoft.Win32.SafeHandles;
using System;

public class SafeHandleWrapper : IDisposable
{
    private SafeFileHandle _handle;
    private bool _disposed;

    public void Dispose()
    {
        if (_disposed) return;
        _handle?.Dispose(); // SafeHandle cleans unmanaged resource
        _disposed = true;
        GC.SuppressFinalize(this);
    }
}

✅ Recommendation: Use SafeHandle instead of raw IntPtr when possible.


8) Weak References (Advanced)

Sometimes you want an object to be collected if memory is tight (e.g., cache):

var strong = new object();
var weak = new WeakReference<object>(strong);

strong = null; // remove strong ref

if (weak.TryGetTarget(out var target))
{
    Console.WriteLine("Object still alive");
}
else
{
    Console.WriteLine("Object collected");
}

9) Best Practices (Checklist ✅)

  1. Always wrap IDisposable objects in using.
  2. Prefer IDisposable + using over finalizers.
  3. Use SafeHandle for OS handles.
  4. Implement Dispose Pattern correctly.
  5. Never call GC.Collect() unless you really know why.
  6. Dispose objects as soon as you’re done (don’t wait for GC).
  7. Make Dispose() idempotent (safe to call multiple times).

10) Quick Interview Q&A

  • What’s the difference between Dispose and Finalize?
    → Dispose is deterministic, Finalize is non-deterministic.

  • Why use GC.SuppressFinalize()?
    → To tell GC the object was already cleaned, so skip finalizer.

  • What is LOH (Large Object Heap)?
    → Heap area for objects > 85KB.

  • When to use WeakReference?
    → For caches — allows GC to reclaim objects under memory pressure.


🎯 Final Thoughts

  • Garbage Collection frees you from manual memory management, but not from resource management.
  • Always use Dispose (and using) for anything that implements IDisposable.
  • Use SafeHandle for unmanaged resources instead of writing finalizers.
  • Finalizers should only be a safety net, not your main cleanup strategy.

Comments

Popular posts from this blog

🏗️ Deep Dive: Understanding Every Concept in Microsoft Entra API Onboarding for .NET Developers

When working with Microsoft Entra (formerly Azure Active Directory), you’ll hear terms like App Registration, Tenant, Client ID, Audience, Scopes, Roles, Tokens, OBO flow , and more. If you’re new, it can feel overwhelming. This guide breaks down every key term and concept , with definitions, examples, and how they connect when you onboard and consume a new API. 🔹 1. Tenant Definition : A tenant in Entra ID is your organization’s dedicated, isolated instance of Microsoft Entra. Think of it like : Your company’s identity directory. Example : contoso.onmicrosoft.com is a tenant for Contoso Ltd. 🔹 2. App Registration Definition : The process of registering an application in Entra to give it an identity and permission to use Microsoft identity platform. Why needed : Without registration, Entra doesn’t know about your app. What it creates : Application (Client) ID – unique identifier for your app Directory (Tenant) ID – your organization’s ID Types of apps : Web ...

☁️ Azure Key vault Short Notes

🟢 What is Azure Key Vault? A cloud service for securely storing and accessing secrets, keys, and certificates . Removes the need to keep secrets (like connection strings, passwords, API keys) inside code or config files. Provides centralized secret management, encryption, and access control . 👉 Think of it like a secure password manager but for your applications. 🟢 Key Features Secrets → store text values (e.g., DB connection string, API key). Keys → store cryptographic keys (RSA, EC) for encryption, signing. Certificates → store/manage SSL/TLS certificates. Access Control → Access Policies (older model). Azure RBAC (modern, preferred). Integration → works with App Service, Functions, AKS, VMs, SQL DB, etc. Logging → audit who accessed secrets via Azure Monitor / Diagnostic Logs. 🟢 Why Use Key Vault? Security → secrets are encrypted with HSM (Hardware Security Modules). Compliance → meet industry standards (PCI-DSS, ISO, GDPR). Automation → aut...