Memory management and resource management are two concepts that are easy to confuse when first starting out is software development. Knowing the difference between them is important to write bug-free, maintainable software.

 

The objective of this article is to discuss the differences between the two concepts and to clear up any confusion.

 

 

Memory management

Memory is a managed resource – in other words, it's under the direct control of the runtime. When an object goes out of scope and is no longer referenced, the garbage collector automatically clears up the memory, eventually. Thus, as developers, we don't really need to worry about explicitly releasing memory.


It is important to note that, although you could manually call the static method GC.Collect() to instruct the garbage collector to deallocate memory, it is generally not advised as it would mostly do more harm than good.


The exact way garbage collection happens is out of the scope of this blog post, but in short: objects in the heap memory go through a marking phase, in which objects that are still referenced get “marked” to not get garbage collected. Then, we move on to the second phase, in which unmarked objects get garbage collected.

 

 

Resource management

On the other hand, resources such as file handles or open network connections are known as unmanaged resources – they run outside the .NET runtime. Such resources are not automatically “managed” by the CLR (Common Language Runtime), so they must be handled by the developer.


The main way to handle such resources in .NET is by implementing the “IDisposable” interface (we’ll get to that further ahead in this article). This way, the consumer of the object knows that it must be explicitly disposed of when it's no longer needed.  This is known as deterministic resource management, meaning it allows the user to deterministically deallocate the resource.


As a side note, it is preferable when trying to dispose of an object to use the “using” keyword, to ensure that the “Dispose” method is called even if an exception occurs within the block. Otherwise, wrap the object inside a “try catch” and call Dispose() inside the finally block.

 

using (var disposableObject = new SomeDisposableObject())
{
// Code that uses disposableObject
}

Instead of:


var disposableObject = new SomeDisposableObject();
try
{
// Code that uses disposableObject
}
catch (Exception ex)
{
}
finally
{
    disposableObject.Dispose();
}

 

The other way resources get freed up is by finalizers (destructors). Finalizers are called in the garbage collection phase when the consumer doesn’t properly dispose of an object that owns unmanaged resources. This is known as non-deterministic resource management, as the user doesn’t exactly know when the resource will be deallocated.


It's important to note that finalizers introduce additional computational burden and should generally be avoided.

 

 

Implementing the “IDisposable” pattern

The reason why I’ve left out the implementation of the IDisposable pattern until now, is that we needed to know about finalizers.

 

There are generally two cases:

  • You only have a class holding other managed resources, managed as in objects that implement IDisposable themselves (file streams, database connections, etc.). In most situations, this is the common scenario.
  • You have a class holding an unmanaged resource directly (resources that belong to the OS, like file handles, window handles, among others).

First case

The implementation is straightforward:

 

public class MyResource : IDisposable
{
    public void Dispose()
    {
            // Clean up managed resources, call Dispose on member variables..
    }

 

Second case

We need to implement a finalizer, but before we get there, it's important to note that the need to explicitly implement a finalizer and directly handle unmanaged resources is relatively rare. Most of the time, we use existing types (like FileStream, HttpClient…) that already handle resource management correctly.


So, unless you are maintaining legacy code that involve dealing with older APIs or work on low-level library where direct resource management is necessary, you will probably only have to deal with the first case.


As for the code for implementing a finalizer:

 

public class MyResource : IDisposable
{
    private bool _disposed = false;

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                // Clean up managed resources, dispose other IDisposable objects here
            }

            // Clean up unmanaged resources (release native resources, file handles etc)

            _disposed = true;
        }
    }

    // Finalizer
    ~MyResource()
    {
        Dispose(false);
    }
}

 

A few notes about this:

  • When Dispose(true) is called, it indicates that the object is being explicitly disposed with the Dispose() method. In this case, we should release both managed and unmanaged resources.
  • As explained previously, the finalizer runs during garbage collection as a safety net in case the consumer of the class doesn’t call Dispose() explicitly. In it, we call Dispose(false) to release only unmanaged resources. By passing false, we avoid re-entering the managed code (since the garbage collector is already cleaning up), thus preventing potential issues like accessing disposed managed objects.
  • We call GC.SuppressFinalize(this) in the Dispose() method GC.SuppressFinalize(this) to inform the garbage collector that the finalizer for this object should not be executed, avoiding unnecessary overhead during garbage collection.

Note: it’s crucial to follow this pattern to prevent resource leaks and ensure proper cleanup.

 

 

For a deeper dive...
Udostępnij ten artykuł