In .NET, memory management is primarily handled by the Garbage Collector (GC). As developers, we usually don’t need to manually allocate and free memory as in C or C++. However, it is crucial to understand how garbage collection works in C#, because it impacts performance, resource management, and application stability.
Garbage Collection is the process of automatically freeing memory occupied by objects that are no longer accessible by the application. The CLR (Common Language Runtime) manages this process and ensures efficient use of system memory.
The .NET Garbage Collector (GC) is designed to manage memory efficiently by automatically reclaiming unused objects. To optimize performance, .NET organizes objects into generations, which represent the "age" of objects in memory. The assumption is that most objects die young (e.g., temporary variables, short-lived results), while a few live longer (e.g., application-level caches, singleton services).
A finalizer is a method that is called by the GC before reclaiming an object’s memory. It is defined using the ~ClassName()
syntax.
using System;
class MyClass
{
~MyClass() // Finalizer
{
Console.WriteLine("Finalizer called, cleaning up unmanaged resources...");
}
}
class Program
{
static void Main()
{
MyClass obj = new MyClass();
obj = null;
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
If your class only uses managed resources (like string
, int
, List
etc.), you don’t need to implement IDisposable
or write a finalizer. The GC will take care of everything.
class Student
{
public string Name { get; set; }
public int Age { get; set; }
}
class Program
{
static void Main()
{
Student s = new Student { Name = "John", Age = 20 };
// No need for cleanup, GC handles it automatically.
}
}
Although not recommended in most scenarios, you can force GC manually:
using System;
class Program
{
static void Main()
{
Console.WriteLine("Memory before collection: " + GC.GetTotalMemory(false));
// Create large objects
byte[] data = new byte[1024 * 1024 * 10]; // 10 MB
Console.WriteLine("Memory after allocation: " + GC.GetTotalMemory(false));
data = null; // Release reference
GC.Collect(); // Force garbage collection
GC.WaitForPendingFinalizers();
Console.WriteLine("Memory after GC: " + GC.GetTotalMemory(true));
}
}
In C#, the garbage collector (GC) automatically manages memory for managed resources. However, unmanaged resources like file handles, database connections, sockets, or OS-level handles must be released manually. For this, we use the IDisposable
interface and the Dispose()
method.
So, instead of relying only on GC, .NET provides the IDisposable interface for deterministic cleanup of unmanaged resources.
using System;
using System.IO;
class Program
{
static void Main()
{
// Using ensures Dispose() is called automatically
using (StreamWriter writer = new StreamWriter("test.txt"))
{
writer.WriteLine("Hello Garbage Collection!");
} // Dispose() called here
}
}
StreamWriter
works with a file handle, which is an unmanaged resource.using
block, the Dispose()
method is called immediately after the block ends, ensuring the file is properly closed at the right time.IDisposable
complements Garbage Collection by giving developers precise control over the release of unmanaged resources.The IDisposable
interface defines one method:
public interface IDisposable
{
void Dispose();
}
When a class implements IDisposable
, it signals that the class holds unmanaged resources and must provide a way to release them.
The recommended dispose pattern includes:
Dispose()
method to free both managed and unmanaged resources.Dispose(bool disposing)
method where the real cleanup happens.~ClassName()
) as a safety net in case Dispose()
is not called.Unmanaged resources are things like file handles, database connections, sockets, or OS handles. They must be released explicitly. Here’s how to do it properly:
using System;
using System.Runtime.InteropServices;
public class UnmanagedWrapper : IDisposable
{
private IntPtr unmanagedResource; // Example unmanaged resource
private bool disposed = false; // To avoid double dispose
// Public Dispose method
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // Prevent finalizer from running
}
// Protected virtual dispose method
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// Free managed resources here
// Example: managedObject.Dispose();
}
// Free unmanaged resources here
if (unmanagedResource != IntPtr.Zero)
{
// Release unmanaged handle
unmanagedResource = IntPtr.Zero;
}
disposed = true;
}
}
// Finalizer (destructor)
~UnmanagedWrapper()
{
Dispose(false);
}
}
class Program
{
static void Main()
{
using (var resource = new UnmanagedWrapper(1024))
{
Console.WriteLine("Using unmanaged resource...");
}
}
}
You can suppress the finalizer if resources are already cleaned up manually:
using System;
class Resource : IDisposable
{
~Resource()
{
Console.WriteLine("Finalizer called.");
}
public void Dispose()
{
Console.WriteLine("Dispose called.");
GC.SuppressFinalize(this); // Prevents finalizer from running
}
}
class Program
{
static void Main()
{
using (Resource r = new Resource())
{
Console.WriteLine("Using resource...");
}
}
}
disposing
Variable Do?The disposing
parameter indicates whether the method was called explicitly via Dispose()
or by the finalizer:
disposing = true
: Called from Dispose()
. Both managed and unmanaged resources can be safely released, since other managed objects are still accessible.disposing = false
: Called from the finalizer (~UnmanagedWrapper()
). At this point, you should only release unmanaged resources. Managed objects may already have been finalized, so accessing them can cause exceptions.No, you cannot reliably access managed data members inside a finalizer. The garbage collector does not guarantee the order in which managed objects are finalized. By the time your finalizer runs, other managed objects may already be cleaned up, so dereferencing them could throw exceptions or behave unpredictably.
using
for disposable objects.IDisposable
.IDisposable
if your class only contains managed resources.IDisposable
when working with unmanaged resources.GC.SuppressFinalize(this)
inside Dispose()
to avoid unnecessary finalization overhead.Dispose(bool disposing)
is protected virtual
so derived classes can override it.using
statement (using var obj = new UnmanagedWrapper();
) to ensure automatic cleanup.
Garbage Collection in .NET is a powerful feature that simplifies memory management. However, understanding its inner workings, along with finalizers and the IDisposable
pattern, allows developers to write efficient and safe C# applications. Always remember: if your class uses only managed resources, let the GC handle it; if it uses unmanaged resources, implement proper cleanup. The Dispose pattern ensures safe and efficient resource management in .NET. By separating managed and unmanaged cleanup with the disposing
flag, you protect your code from errors during garbage collection and provide developers with explicit control over resource lifetime.
Learn C#, ASP.NET Core, MVC, Blazor, SQL Server and more with personalized guidance from a Microsoft Certified Professional with 20+ years of experience. All classes are 1-on-1, ensuring you get the attention you deserve. Whether you're a beginner or preparing for interviews, I help you build solid fundamentals and real-world confidence.
🚀 Book a Free ConsultationEmail: supernova.software.solutions@gmail.com
Website: supernovaservices.com
Location: Dum Dum, Kolkata, India
To keep every session productive and distraction-free, please follow these simple guidelines:
Following these guidelines helps you focus better and ensures I can deliver the best learning experience in every class.
I prefer to start with a short 10-minute free call so I can understand:
Why? Because course content, teaching pace, and fees all depend on your needs — there’s no “one-size-fits-all” pricing. Please leave your details below, and I’ll get back to you to arrange a convenient time for the call.