Roadmap › .NET Internals — CLR, CTS, CLS, IL, JIT, Memory Management & Garbage Collection › Module: Memory Management
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):
| Cause | Example | Fix |
|---|---|---|
| Static/singleton collections that only grow | static List<Order> _all | Bounded caches (MemoryCache with size limits/expiry) |
| Event handlers not unsubscribed | Short-lived object subscribes to a long-lived publisher's event | -= in Dispose, weak events |
| Captured variables in long-lived lambdas/timers | Timer callback capturing a big object | Dispose timers, avoid capturing |
| Wrong DI lifetimes | Scoped DbContext captured inside a singleton ("captive dependency") | Respect lifetimes; ValidateScopes |
HttpClient per request | Socket exhaustion (not memory, but similar symptom) | IHttpClientFactory |
| Not disposing unmanaged resources | Streams, Bitmap, connections | using |
| LOH fragmentation | Many large temporary arrays | ArrayPool<T>, RecyclableMemoryStream, reuse buffers |
| Pinned objects | Long-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.