Search

Avoid C# Memory Leaks with Destructor and Dispose

0 views

Why Some Classes Need a Destructor and Dispose

When you write C# code that talks to the outside world - files, databases, sockets, or any unmanaged resource - the objects you create can hold onto memory that the garbage collector can’t reclaim on its own. Think of a file handle as a doorway to an external system; closing that doorway is the responsibility of the code that opened it. If you forget to close it, the doorway remains open, and the system keeps a handle that it must eventually clean up. The garbage collector knows nothing about that external handle, so it only reclaims the memory that the .NET runtime uses internally. This mismatch is the root cause of most memory leaks in C# applications.

Two different patterns illustrate how developers handle this mismatch. The first pattern involves a class that creates several resources in its constructor - such as a third‑party object, a file stream, a database connection, and a network socket. Since the constructor takes the responsibility of allocating resources, the class also needs a mechanism to release them when the object is no longer needed. That mechanism is a destructor (also called a finalizer) and the IDisposable interface. The second pattern places resource allocation inside a dedicated method. The method wraps the allocation in a try block and frees everything in a finally block. This approach keeps the constructor lightweight and eliminates the need for a destructor or IDisposable because the cleanup logic is guaranteed to run as part of the method’s control flow.

Consider the following simplified example of the first pattern:

Prompt
class MustDispose : IDisposable</p> <p>{</p> <p> private ThirdPartyObject _tp;</p> <p> private FileStream _file;</p> <p> private IntPtr _memory;</p> <p> private DbConnection _db;</p> <p> private Socket _socket;</p> <p> public MustDispose()</p> <p> {</p> <p> _tp = new ThirdPartyObject();</p> <p> _file = new FileStream("log.txt", FileMode.OpenOrCreate);</p> <p> _memory = AllocateMemory(1024);</p> <p> _db = new SqlConnection("ConnectionString");</p> <p> _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);</p> <p> }</p> <p> ~MustDispose()</p> <p> {</p> <p> // Finalizer runs only if Dispose hasn't been called.</p> <p> if (_tp != null) _tp.Dispose();</p> <p> if (_file != null) _file.Dispose();</p> <p> if (_memory != IntPtr.Zero) FreeMemory(_memory);</p> <p> if (_db != null) _db.Close();</p> <p> if (_socket != null) _socket.Close();</p> <p> }</p> <p> public void Dispose()</p> <p> {</p> <p> // Dispose performs cleanup immediately.</p> <p> // Tell the GC that the finalizer is no longer needed.</p> <p> GC.SuppressFinalize(this);</p> <p> }</p> <p>}</p>

The second pattern, where cleanup happens in a method, looks like this:

Prompt
class NoDispose</p> <p>{</p> <p> public void PerformOperation()</p> <p> {</p> <p> ThirdPartyObject tp = null;</p> <p> FileStream file = null;</p> <p> IntPtr memory = IntPtr.Zero;</p> <p> DbConnection db = null;</p> <p> Socket socket = null;</p> <p> try</p> <p> {</p> <p> tp = new ThirdPartyObject();</p> <p> file = new FileStream("log.txt", FileMode.OpenOrCreate);</p> <p> memory = AllocateMemory(1024);</p> <p> db = new SqlConnection("ConnectionString");</p> <p> socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);</p> <p> // ... do work ...</p> <p> }</p> <p> finally</p> <p> {</p> <p> if (tp != null) tp.Dispose();</p> <p> if (file != null) file.Dispose();</p> <p> if (memory != IntPtr.Zero) FreeMemory(memory);</p> <p> if (db != null) db.Close();</p> <p> if (socket != null) socket.Close();</p> <p> }</p> <p> }</p> <p>}</p>

The choice between these patterns boils down to who owns the resources and when they must be released. If a class owns resources from the moment it is constructed, you must expose a public Dispose method and a finalizer to guard against forgotten calls. If resources are acquired only inside a single method and you can guarantee that the method always cleans up before returning, the finalizer and IDisposable are unnecessary. The latter pattern reduces the surface area for bugs but restricts flexibility, especially when you need to share resources across multiple methods.

Both approaches are valid; the key is to match the pattern to the class’s responsibilities. A class that owns resources should give callers a clear contract for releasing them. A class that temporarily borrows resources should manage them internally with try/finally. Understanding this distinction helps you write code that is safe from leaks and easier to maintain.

The Standard Dispose Pattern: How and Why

Once you decide that a class owns unmanaged resources, the .NET community settled on a standard pattern: implement IDisposable and provide a public Dispose method. The pattern also includes a finalizer that the garbage collector calls if the consumer forgot to invoke Dispose. The finalizer is a safety net, not a substitute for proper disposal. It runs on the finalizer thread, which can delay the release of resources and affect application performance. Therefore, calling Dispose as soon as you finish with an object is the preferred approach.

Here is the canonical implementation you will find in many code bases:

Prompt
public class ResourceHolder : IDisposable</p> <p>{</p> <p> private bool _disposed = false; // To detect redundant calls</p> <p> {</p> <p> Dispose(true);</p> <p> GC.SuppressFinalize(this); // Prevent finalizer from running</p> <p> }</p> <p> protected virtual void Dispose(bool disposing)</p> <p> {</p> <p> if (_disposed) return;</p> <p> if (disposing)</p> <p> {</p> <p> // Free managed objects here.</p> <p> // Example: _managedResource?.Dispose();</p> <p> }</p> <p> // Free unmanaged resources here.</p> <p> // Example: if (_unmanagedPtr != IntPtr.Zero) FreeMemory(_unmanagedPtr);</p> <p> _disposed = true;</p> <p> }</p> <p> ~ResourceHolder()</p> <p> {</p> <p> Dispose(false);</p> <p> }</p> <p>}</p>

Notice a few details. The Dispose(bool disposing) method separates cleanup for managed and unmanaged resources. The disposing flag is true when called from Dispose and false when called from the finalizer. Managed objects should only be touched when disposing is true because the finalizer runs after the garbage collector has already reclaimed the managed objects. This pattern also guards against multiple calls to Dispose by tracking the _disposed flag.

When you implement the pattern, think of the responsibilities. Managed resources - such as other IDisposable objects - are freed inside the if (disposing) block. Unmanaged resources - memory allocated with Marshal.AllocHGlobal, file handles obtained from P/Invoke, or database connections that do not implement IDisposable - are freed regardless of the disposing flag. This guarantees that the finalizer can clean up when the consumer forgets.

Sometimes you see an even simpler pattern that skips the Dispose(bool) overload and calls GC.SuppressFinalize directly in Dispose. That works fine for classes that don’t expose other IDisposable fields or that never override Finalize. However, the full pattern is more flexible and aligns with best practices, especially when you plan to subclass the class in the future.

Adopting the standard pattern also means you can rely on language constructs like using to enforce disposal. The compiler automatically generates a try/finally block that calls Dispose at the end of the using scope. This eliminates boilerplate and reduces the risk of leaks. Keep in mind that the using statement only works with objects that implement IDisposable. If your class doesn’t, you’ll need to manage it manually.

In short, implement IDisposable and the full dispose pattern whenever your class owns unmanaged resources or holds references to other IDisposable objects. Follow the pattern strictly, and let the garbage collector handle the rest. This approach keeps your code safe, predictable, and performant.

Best Practices for Using, Disposing, and Avoiding Leaks

Once you know which pattern to use, the next step is to apply it consistently across your code base. The most common mistake is to create an object, forget to dispose it, and let the finalizer run later, sometimes after a long time. That delay can tie up memory, file handles, or database connections for longer than intended, leading to resource starvation under load. The fix is simple: dispose as soon as you finish with an object.

Here are several practical techniques that reinforce good disposal habits:

1. Wrap objects in a using block. The compiler turns using (var obj = new ResourceHolder()) { / work / } into a try/finally that calls obj.Dispose() automatically. It’s concise, readable, and less error‑prone than manual calls.

2. Use using for multiple objects. If you need to open a file and a database connection together, nest the using statements or chain them on the same line. For example: using (var file = new FileStream(...)) using (var db = new SqlConnection(...)) { / work / }. This guarantees that both resources are released in the correct order.

3. Avoid keeping objects in fields when they are short‑lived. If a method needs a file stream only for a few lines, declare it inside the method rather than as a field of a long‑lived class. The field would stay alive as long as the class instance does, potentially holding resources longer than necessary.

4. Prefer try/finally over using when you need more control. For example, if you need to swallow exceptions or perform cleanup after a retry loop, a manual try/finally block gives you that flexibility while still guaranteeing disposal.

5. Check for null before disposing. A null check is cheap and defensive. If you accidentally create an object but an exception throws before initializing a resource, the Dispose method should handle nulls gracefully.

6. Avoid calling GC.SuppressFinalize(this) directly unless you are certain you have a finalizer. This call tells the garbage collector that the finalizer won’t run, which is only correct if you’ve already disposed. In the standard pattern, it lives inside Dispose after you’ve cleaned up.

7. Use IDisposable in your public APIs. If a method returns an object that holds unmanaged resources, document that the caller must dispose of the returned object. For example: IEnumerable<FileInfo> GetFiles(string path) => Directory.EnumerateFiles(path).Select(...). The consumer can then wrap the enumeration in a using block if the enumeration itself implements IDisposable

8. Keep the finalizer lightweight. If you decide to keep a finalizer for safety, make it minimal. The finalizer should only release unmanaged resources that haven’t been cleaned up, not perform expensive logic.

9. Profile your application. Use tools like Visual Studio’s Diagnostic Tools, dotMemory, or PerfView to detect unreleased handles or memory that never gets freed. Look for patterns where a class holds onto a resource longer than necessary.

10. Educate your team. Make disposal a part of code reviews. A quick check for missing using blocks or a missing Dispose call can prevent leaks before they surface in production.

By following these habits, you ensure that your applications remain responsive, use resources efficiently, and avoid the subtle bugs that arise from forgotten cleanup. Remember that the garbage collector is great at reclaiming memory, but it has no knowledge of external handles. Treat those handles as first‑class citizens and clean them up promptly, and your code will stay healthy for years.

Suggest a Correction

Found an error or have a suggestion? Let us know and we'll review it.

Share this article

Comments (0)

Please sign in to leave a comment.

No comments yet. Be the first to comment!

Related Articles