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>→ ManagedFileStream,SqlConnection,IntPtr(native pointer) → Wrap unmanaged resources → NeedDispose()
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 ✅)
- Always wrap
IDisposableobjects inusing. - Prefer
IDisposable+usingover finalizers. - Use
SafeHandlefor OS handles. - Implement Dispose Pattern correctly.
- Never call
GC.Collect()unless you really know why. - Dispose objects as soon as you’re done (don’t wait for GC).
- 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(andusing) for anything that implementsIDisposable. - Use
SafeHandlefor unmanaged resources instead of writing finalizers. - Finalizers should only be a safety net, not your main cleanup strategy.
Comments
Post a Comment