Lessons in this course (11)

8. Memory leaks in .NET and how to find them

Advanced★ Must knowLesson 8 of 11

Common causes of managed "leaks" (objects kept reachable):

CauseExampleFix
Static/singleton collections that only growstatic List<Order> _allBounded caches (MemoryCache with size limits/expiry)
Event handlers not unsubscribedShort-lived object subscribes to a long-lived publisher's event-= in Dispose, weak events
Captured variables in long-lived lambdas/timersTimer callback capturing a big objectDispose timers, avoid capturing
Wrong DI lifetimesScoped DbContext captured inside a singleton ("captive dependency")Respect lifetimes; ValidateScopes
HttpClient per requestSocket exhaustion (not memory, but similar symptom)IHttpClientFactory
Not disposing unmanaged resourcesStreams, Bitmap, connectionsusing
LOH fragmentationMany large temporary arraysArrayPool<T>, RecyclableMemoryStream, reuse buffers
Pinned objectsLong-lived GCHandle.Alloc(..., Pinned)POH or Memory<T>/Span<T> patterns

Investigation steps:

bash
dotnet-counters monitor -p <pid> --counters System.Runtime     # watch GC heap size, gen-2 count, LOH size over time
dotnet-gcdump collect -p <pid>                                  # heap snapshot → open in Visual Studio / PerfView
dotnet-dump collect -p <pid> && dotnet-dump analyze <file>      # > dumpheap -stat ; > gcroot <address>

Take two snapshots some minutes apart, compare object counts by type, then use gcroot (or "paths to root" in the profiler) to see who is holding the growing objects.