Skip to Content
Course content

82: Garbage Collection in .NET

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I want to show you a piece of code that looks perfectly fine at first glance. It's a simple session tracker for a hypothetical application. I've seen this pattern in professional codebases more times than I care to admit, and it almost always leads to a frantic 2:00 AM call about the server crashing with an OutOfMemoryException.

public class UserSession
{
    public string Username { get; set; }
    public byte[] SessionData { get; set; } = new byte[1024 * 1024]; // 1MB of data
}

public static class SessionTracker
{
    // We want to keep track of everyone who has logged in for analytics
    private static readonly List<UserSession> _allSessions = new List<UserSession>();

    public static void TrackSession(UserSession session)
    {
        _allSessions.Add(session);
    }
}

// Usage in the app
while (true)
{
    var session = new UserSession { Username = "User" + Guid.NewGuid() };
    SessionTracker.TrackSession(session);
    // The 'session' variable goes out of scope here. 
    // You'd think the GC would reclaim it eventually...
}

The invisible anchor causing memory leaks

If you run this, your RAM usage will climb steadily until the app crashes. You might be thinking, "Wait, C# has a Garbage Collector (GC). Why isn't it cleaning up the UserSession objects once the loop iteration ends?"

Here is the deal: the GC isn't a magic wand that deletes everything you're "done" with; it's a reachability analyzer. It starts at the "roots"—things like static variables, local variables currently on the stack, and CPU registers. If the GC can find a path from a root to an object, that object is considered "live" and will not be collected.

In the code above, the _allSessions list is static. That means it is a root. Because the list holds a reference to every UserSession ever created, those objects are effectively anchored to the application's lifetime. Even though your loop has moved on and you no longer have a local variable pointing to that session, the static list still does. You've created a memory leak in a managed language.

Breaking the chain with WeakReferences

Sometimes you want to keep track of an object, but you don't want that tracking to prevent the GC from reclaiming the memory if nothing else is using the object. This is where WeakReference<T> comes in.

public static class SessionTracker
{
    // We store WeakReferences instead of the objects themselves
    private static readonly List<WeakReference<UserSession>> _allSessions = new List<WeakReference<UserSession>>();

    public static void TrackSession(UserSession session)
    {
        _allSessions.Add(new WeakReference<UserSession>(session));
    }

    public static int GetActiveSessionCount()
    {
        int count = 0;
        foreach (var reference in _allSessions)
        {
            if (reference.TryGetTarget(out _))
            {
                count++;
            }
        }
        return count;
    }
}

By using WeakReference, we're telling the GC: "I'm interested in this object, but if you're the only one who wants it, go ahead and take it." The next time the GC runs a collection, it will see that the only remaining references to those UserSession objects are "weak," and it will reclaim that memory. When we call TryGetTarget, it will simply return false if the object has been collected.

Understanding the generational hustle

To use the GC effectively, you need to understand that it doesn't scan every single object in your heap every time it runs—that would be incredibly slow. Instead, .NET uses a generational approach:

  • Generation 0: Where all short-lived objects start. This is collected most frequently.
  • Generation 1: The "buffer" zone. If an object survives a Gen 0 collection, it gets promoted here.
  • Generation 2: The long-haul zone. Objects that survive Gen 1 end up here. Collections here (Full GCs) are expensive and can cause "stop-the-world" pauses where your app freezes for a moment.

I've seen developers try to "help" the GC by calling GC.Collect() manually. In 99% of cases, don't do this. The GC is highly optimized and knows better than we do when to trigger a collection based on memory pressure and allocation patterns. Forcing a collection often pushes short-lived objects into Gen 1 or Gen 2 prematurely, actually making your app slower and increasing memory fragmentation.

The Large Object Heap (LOH) exception

There is one special case you should know about: the Large Object Heap. Any object larger than 85,000 bytes (like our 1MB SessionData array) goes straight to the LOH. The LOH is treated as part of Generation 2, but it's handled differently because moving massive chunks of memory around to compact them is too expensive. This is why you should be cautious about allocating huge arrays frequently; you can end up with "holes" in your memory that the GC can't easily fill, leading to an OutOfMemoryException even if you have total free memory available.




📋 Practical Task

Fixing the Cache Memory Leak

You've been handed a DataCache class that is causing the application to consume gigabytes of RAM over time. The developer who wrote it intended for the cache to hold onto Document objects, but they used a static Dictionary that never lets go of anything, even when the rest of the application has finished processing the document.

Your Task: Rewrite the DataCache class to use WeakReference<Document> so that the cache does not prevent the Garbage Collector from reclaiming documents that are no longer in use elsewhere in the application.

public class Document 
{ 
    public string Id { get; set; } 
    public string Content { get; set; } 
}

public static class DataCache
{
    // BUG: This dictionary keeps every document alive forever
    private static readonly Dictionary<string, Document> _cache = new Dictionary<string, Document>();

    public static void AddToCache(Document doc)
    {
        _cache[doc.Id] = doc;
    }

    public static Document GetFromCache(string id)
    {
        if (_cache.ContainsKey(id))
        {
            return _cache[id];
        }
        return null;
    }
}

Requirements:

  • Modify the internal storage to use WeakReference<Document>.
  • Update GetFromCache to return the Document if it still exists in memory, or null if it has been collected by the GC.
  • Ensure the AddToCache method correctly wraps the document in a weak reference.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.