Understanding C# Memory Management: Weak References, GC Latency Modes, and Unmanaged Resources
Memory management is a crucial aspect of programming in C#. Understanding how garbage collection (GC) works, particularly in relation to weak references, GC latency modes, and unmanaged resources, can help you write more efficient and reliable code. In this article, we will explore these concepts with simple examples using C# top-level statements introduced in C# 6 and later.
C# Weak References
A weak reference allows you to reference an object without preventing it from being collected by the garbage collector. This is useful when you want to keep track of objects that can be reclaimed if memory is needed.
using System;
class Program
{
static void Main()
{
// Create a strong reference to an object
var strongReference = new MyClass();
// Create a weak reference to the same object
WeakReference<MyClass> weakReference = new WeakReference<MyClass>(strongReference);
// Check if the weak reference is still alive
if (weakReference.TryGetTarget(out MyClass target))
{
Console.WriteLine("Object is still alive.");
}
else
{
Console.WriteLine("Object has been collected.");
}
// Remove the strong reference
strongReference = null;
// Force garbage collection
GC.Collect();
GC.WaitForPendingFinalizers();
// Check again if the weak reference is still alive
if (weakReference.TryGetTarget(out target))
{
Console.WriteLine("Object is still alive.");
}
else
{
Console.WriteLine("Object has been collected.");
}
}
}
class MyClass
{
// Example class
}
In this example, MyClass
is first strongly referenced, then weakly referenced. After removing the strong reference and forcing garbage collection, the weak reference will likely find that the object has been collected.
GC Latency Mode
Garbage Collection (GC) latency mode affects how frequently garbage collection occurs. The .NET runtime provides several latency modes to balance between performance and responsiveness.
GC Latency Modes
- Batch: GC occurs infrequently, optimizing for throughput.
- Interactive: Balances between throughput and responsiveness.
- LowLatency: Minimizes the time spent in GC, useful for real-time applications.
Example: Setting GC Latency Mode
You can change the GC latency mode using the GCSettings.LatencyMode
property:
using System;
using System.Runtime;
class Program
{
static void Main()
{
// Set GC latency mode to LowLatency
GCSettings.LatencyMode = GCLatencyMode.LowLatency;
// Your application code here
// Restore default latency mode
GCSettings.LatencyMode = GCLatencyMode.Interactive;
}
}
In this example, we set the GC latency mode to LowLatency
to reduce GC pauses, and then restore it to Interactive
.
GC Committed Bytes
The term "committed bytes" refers to the amount of memory that has been allocated and is guaranteed to be available for use. This is important for understanding how much memory your application is using.
Example: Checking GC Committed Bytes
You can use the GC.GetTotalMemory
method to get the amount of memory currently used:
using System;
class Program
{
static void Main()
{
// Get total memory committed
long committedBytes = GC.GetTotalMemory(false);
Console.WriteLine($"Committed Bytes: {committedBytes}");
}
}
Here, GC.GetTotalMemory
returns the number of bytes currently allocated. The false
parameter means we don’t force a collection before measuring.
C# Unmanaged Resources
Unmanaged resources are resources not handled by the garbage collector, such as file handles or database connections. You need to explicitly release these resources to avoid leaks.
Example: Using Unmanaged Resources
Here’s how you might work with unmanaged resources:
using System;
using System.IO;
class Program
{
static void Main()
{
// Create a file stream (unmanaged resource)
using (var fileStream = new FileStream("example.txt", FileMode.Create))
{
// Write data to the file
using (var writer = new StreamWriter(fileStream))
{
writer.WriteLine("Hello, world!");
}
} // FileStream is automatically disposed here
}
}
In this example, the FileStream
is an unmanaged resource. Using a using
statement ensures that the FileStream
is disposed of properly, releasing the unmanaged resource.
C# Release All Resources
Properly releasing resources is essential for efficient memory management. For unmanaged resources, you need to ensure they are disposed of. This is typically done using the IDisposable
interface and using
statements.
Example: Implementing IDisposable
Here’s an example of a class that implements IDisposable
:
using System;
class MyResource : IDisposable
{
private bool disposed = false;
public void DoWork()
{
if (disposed) throw new ObjectDisposedException(nameof(MyResource));
// Perform work with the resource
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// Dispose managed resources
}
// Dispose unmanaged resources
disposed = true;
}
}
~MyResource()
{
Dispose(false);
}
}
class Program
{
static void Main()
{
using (var resource = new MyResource())
{
resource.DoWork();
} // MyResource is automatically disposed here
}
}
In this example, MyResource
implements IDisposable
to handle both managed and unmanaged resources. The Dispose
method is called explicitly when the using
block ends, ensuring resources are released.