Skip to main content

7.1 - Memory and Resource Management

Memory management is a critical aspect of C# programming that directly impacts application performance, resource utilization, and overall stability. C# provides automatic memory management through garbage collection while also offering mechanisms for deterministic resource cleanup.

7.1.1 - Garbage Collection

The .NET Garbage Collector (GC) automatically manages memory allocation and release, freeing developers from manual memory management tasks.

7.1.1.1 - How Garbage Collection Works

The garbage collector operates on the following principles:

  1. Allocation: When objects are created, memory is allocated from the managed heap.
  2. Tracking: The runtime maintains information about object references.
  3. Collection: Periodically, the GC identifies objects that are no longer reachable (garbage).
  4. Reclamation: Memory occupied by unreachable objects is reclaimed for future allocations.
void CreateObjects()
{
// Object is created and allocated on the managed heap
var largeObject = new byte[1000000];

// When this method exits, largeObject becomes unreachable
// The GC will eventually reclaim this memory
}

7.1.1.2 - Triggering Garbage Collection

While the GC runs automatically, it can be explicitly triggered (though this is generally not recommended in production code):

// Force garbage collection
GC.Collect();

// Force collection of a specific generation
GC.Collect(2); // Collect generation 2 and below

7.1.2 - Generations and Performance Implications

The .NET GC uses a generational approach to improve collection efficiency.

7.1.2.1 - The Generational Hypothesis

The GC is built on two empirical observations:

  • Most objects die young (become garbage shortly after allocation)
  • Older objects tend to live longer

7.1.2.2 - The Three Generations

Objects in the managed heap are organized into three generations:

GenerationDescriptionCollection Frequency
Generation 0Newly allocated objectsMost frequent
Generation 1Objects that survive a Gen 0 collectionLess frequent
Generation 2Long-lived objectsLeast frequent
// Check the generation of an object
object obj = new object();
int generation = GC.GetGeneration(obj);
Console.WriteLine($"Object is in generation {generation}");

7.1.2.3 - Performance Considerations

  • Gen 0 collections are fast but pause the application briefly
  • Gen 2 collections are more expensive and can cause noticeable pauses
  • Large Object Heap (LOH) stores objects larger than 85,000 bytes and has special collection rules
// Get memory statistics
Console.WriteLine($"Total memory: {GC.GetTotalMemory(false)} bytes");
Console.WriteLine($"Max generation: {GC.MaxGeneration}");

7.1.3 - IDisposable and Finalizers

For resources that require explicit cleanup (like file handles, network connections, etc.), C# provides two mechanisms: the IDisposable interface and finalizers.

7.1.3.1 - The IDisposable Interface

IDisposable enables deterministic resource cleanup through its single method:

public interface IDisposable
{
void Dispose();
}

Example implementation:

public class ResourceHolder : IDisposable
{
private bool _disposed = false;
private FileStream _fileStream;

public ResourceHolder(string filePath)
{
_fileStream = new FileStream(filePath, FileMode.Open);
}

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

protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Dispose managed resources
_fileStream?.Dispose();
}

// Free unmanaged resources

_disposed = true;
}
}
}

7.1.3.2 - Finalizers

Finalizers (also called destructors) provide a safety net for cleaning up unmanaged resources when Dispose() isn't called:

public class ResourceHolder : IDisposable
{
private FileStream _fileStream;
private bool _disposed = false;

// Constructor
public ResourceHolder(string filePath)
{
_fileStream = new FileStream(filePath, FileMode.Open);
}

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

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

protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Dispose managed resources
_fileStream?.Dispose();
}

// Free unmanaged resources

_disposed = true;
}
}
}

Important: Finalizers have several drawbacks:

  • They run on the finalizer thread, which can impact performance
  • There's no guarantee when they'll run
  • Objects with finalizers require two GC cycles to be fully collected

7.1.4 - Implementing the Dispose Pattern

The standard Dispose Pattern combines IDisposable and finalizers to ensure proper resource cleanup.

7.1.4.1 - Basic Dispose Pattern

public class DisposableResource : IDisposable
{
private bool _disposed = false;
private IntPtr _nativeResource = IntPtr.Zero;
private ManagedResource _managedResource;

// Constructor
public DisposableResource()
{
_nativeResource = AllocateNativeResource();
_managedResource = new ManagedResource();
}

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

// Public implementation of Dispose
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

// Protected implementation of Dispose
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Dispose managed resources
_managedResource.Dispose();
}

// Free unmanaged resources
if (_nativeResource != IntPtr.Zero)
{
FreeNativeResource(_nativeResource);
_nativeResource = IntPtr.Zero;
}

_disposed = true;
}
}

// Method to allocate native resource (placeholder)
private IntPtr AllocateNativeResource()
{
return new IntPtr(1);
}

// Method to free native resource (placeholder)
private void FreeNativeResource(IntPtr resource)
{
// Free the resource
}

// Class representing a managed resource
private class ManagedResource : IDisposable
{
public void Dispose()
{
// Cleanup code
}
}
}

7.1.4.2 - Dispose Pattern for Derived Classes

When implementing the Dispose pattern in a class hierarchy:

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

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

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

protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Dispose managed resources
}

// Free unmanaged resources

_disposed = true;
}
}
}

public class DerivedClass : BaseClass
{
private bool _disposed = false;
private ManagedResource _resource;

public DerivedClass()
{
_resource = new ManagedResource();
}

// No finalizer needed unless DerivedClass has its own unmanaged resources

protected override void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Dispose derived class's managed resources
_resource.Dispose();
}

// Free derived class's unmanaged resources

_disposed = true;
}

// Call base class's Dispose
base.Dispose(disposing);
}

private class ManagedResource : IDisposable
{
public void Dispose()
{
// Cleanup code
}
}
}

7.1.5 - Using using Statements

The using statement provides a convenient syntax for working with IDisposable objects, ensuring Dispose() is called even if an exception occurs.

7.1.5.1 - Basic using Statement

public void ProcessFile(string path)
{
using (var file = new StreamReader(path))
{
string content = file.ReadToEnd();
// Process content
} // file.Dispose() is automatically called here
}

7.1.5.2 - Multiple Resources

public void CopyFile(string sourcePath, string destinationPath)
{
using (var sourceFile = new StreamReader(sourcePath))
using (var destinationFile = new StreamWriter(destinationPath))
{
string content = sourceFile.ReadToEnd();
destinationFile.Write(content);
} // Both resources are disposed here
}

7.1.5.3 - Using Declaration (C# 8.0+)

In C# 8.0 and later, you can use the simplified using declaration:

public void ProcessFile(string path)
{
using var file = new StreamReader(path);
string content = file.ReadToEnd();
// Process content

// file.Dispose() is called at the end of the enclosing scope
}

7.1.6 - Memory Management Best Practices

7.1.6.1 - General Guidelines

  1. Implement IDisposable for classes that own disposable resources
  2. Call Dispose explicitly when you're done with a resource
  3. Use using statements whenever possible
  4. Avoid finalizers unless absolutely necessary
  5. Be cautious with large objects to minimize pressure on the Large Object Heap
  6. Avoid explicit GC.Collect calls in production code

7.1.6.2 - Avoiding Memory Leaks

Common causes of memory leaks in C# applications:

  1. Event handlers not unsubscribed:

    // Potential memory leak
    public void Subscribe()
    {
    SomeStaticEvent += OnEventTriggered;
    }

    // Proper cleanup
    public void Unsubscribe()
    {
    SomeStaticEvent -= OnEventTriggered;
    }
  2. Captured variables in closures:

    // Potential memory leak if stored in a long-lived collection
    public void CreateAction()
    {
    var largeObject = new byte[1000000];
    Action action = () => Console.WriteLine(largeObject.Length);
    _actions.Add(action); // largeObject is captured and retained
    }
  3. Static collections that grow unbounded:

    // Potential memory leak
    public static class Cache
    {
    private static Dictionary<string, object> _items = new Dictionary<string, object>();

    public static void Add(string key, object value)
    {
    _items[key] = value; // Items are never removed
    }
    }

7.1.6.3 - Tools for Memory Analysis

  • Visual Studio Diagnostics Tools: Memory usage snapshots and analysis
  • dotMemory: Detailed memory profiling
  • PerfView: Performance and memory analysis tool from Microsoft

7.1.6.4 - Weak References

Weak references allow the garbage collector to collect an object while still maintaining a reference to it:

public class Cache
{
private Dictionary<string, WeakReference<object>> _cache = new Dictionary<string, WeakReference<object>>();

public void Add(string key, object value)
{
_cache[key] = new WeakReference<object>(value);
}

public bool TryGetValue(string key, out object value)
{
value = null;

if (_cache.TryGetValue(key, out var weakRef))
{
return weakRef.TryGetTarget(out value);
}

return false;
}
}

Summary

Effective memory management in C# requires understanding both automatic (garbage collection) and manual (IDisposable) mechanisms. By following the Dispose pattern, using the using statement, and adhering to best practices, you can create applications that use memory efficiently and avoid resource leaks.

Additional Resources