C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
82: Garbage Collection in .NET
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
GetFromCacheto return theDocumentif it still exists in memory, ornullif it has been collected by the GC. - Ensure the
AddToCachemethod correctly wraps the document in a weak reference.
There are no comments for now.