Memory & Garbage Collection
18 questions foundThe GC automatically reclaims unreachable managed objects. It is generational (Gen 0/1/2): short-lived objects are collected cheaply in Gen 0, survivors are promoted.
var tmp = new byte[1024]; // Gen 0, collected quickly when unreachable
Real-world example Most request-scoped allocations die in Gen 0, keeping collections cheap.
Managed resources are handled by the GC; unmanaged resources (file handles, sockets, native memory) are not and must be released via Dispose/finalizers.
using var file = File.OpenRead(path); // wraps an unmanaged handle
Real-world example Wrapping a native handle in an IDisposable so it's freed deterministically.
It provides deterministic cleanup of unmanaged resources via Dispose(), usually with using. Implement it when a type owns unmanaged or disposable resources.
public void Dispose() { _conn.Dispose(); GC.SuppressFinalize(this); }
Real-world example A repository that owns a DbConnection implements IDisposable so callers can release it promptly.
Dispose is deterministic cleanup you call; a finalizer runs non-deterministically during GC as a safety net. Finalizers delay collection, so prefer Dispose and suppress finalization.
~Handle() { ReleaseNative(); } // last resort
public void Dispose() { ReleaseNative(); GC.SuppressFinalize(this); }
Real-world example A wrapper around native memory uses a finalizer only as a backstop if Dispose was missed.
Objects stay reachable unintentionally: static references, un-removed event handlers, long-lived caches, or captured closures. The GC can't collect what is still referenced.
StaticCache.Add(bigObject); // never removed -> leak
Real-world example An ever-growing static dictionary cache slowly exhausts memory in a long-running service.
Span<T> gives allocation-free views over existing memory (arrays, stack, native); ArrayPool<T> rents and returns buffers instead of allocating. Both cut Gen 0 churn on hot paths.
var buffer = ArrayPool<byte>.Shared.Rent(4096);
try { Read(buffer); } finally { ArrayPool<byte>.Shared.Return(buffer); }
Real-world example A high-throughput parser reuses pooled buffers to avoid allocating per request.
The stack stores value-type local variables and method call frames, with fast, automatic, LIFO-ordered allocation/deallocation tied to method scope; the heap stores reference-type objects (and boxed value types), with allocation managed by the .NET runtime and deallocation handled by the garbage collector, not tied to any particular method's scope.
void Method() {
int x = 5; // value type: allocated on the stack
var person = new Person(); // reference type: 'person' variable on stack, actual object on the heap
} // 'x' and the 'person' reference are popped off the stack when Method() returns
Real-world example Understanding why a local int variable is instantly reclaimed on method return, while a 'new Person()' object survives until the GC decides to collect it.
Common follow-ups: Why can objects on the heap potentially survive LONGER than the method call that created them?
Value vs Reference Types
The GC divides objects into Generation 0 (newest, short-lived objects), Generation 1 (a buffer generation), and Generation 2 (long-lived objects) — based on the 'generational hypothesis' that MOST objects die young, so the GC checks Gen 0 frequently and cheaply, only occasionally promoting survivors to older generations, which are scanned far less often.
// Conceptual: objects are checked increasingly less often as they survive collections
var temp = new byte[100]; // likely Gen 0, collected quickly if short-lived
static readonly Cache cache = new Cache(); // promoted to Gen 2 over time, rarely re-scanned
Real-world example Understanding why creating many short-lived temporary objects in a loop is generally cheap, since Gen 0 collections are fast and frequent.
Common follow-ups: What triggers a full Generation 2 collection, and why is it considered much more expensive than a Gen 0 collection?
Fundamentals
The garbage collector only manages MANAGED memory — it doesn't know how to release unmanaged resources like file handles, database connections, or OS handles. IDisposable's Dispose() method provides a deterministic way to release those resources explicitly, and 'using' guarantees Dispose() is called automatically when the object goes out of scope, even if an exception occurs.
public class FileWrapper : IDisposable {
private readonly FileStream _stream;
public FileWrapper(string path) { _stream = File.OpenRead(path); }
public void Dispose() => _stream.Dispose(); // releases the unmanaged file handle
}
using (var wrapper = new FileWrapper("data.txt")) { /* ... */ } // Dispose() guaranteed to run
Real-world example Ensuring a database connection, file handle, or network socket is promptly released rather than waiting for the GC's unpredictable timing.
Common follow-ups: What happens to an unmanaged resource if you forget to implement IDisposable and never explicitly release it?
File I/O & Streams
A memory leak happens when objects remain REACHABLE (through a live reference chain from a root, like a static field, an event subscription, or a cached collection) even though the application logically no longer needs them — since the GC only collects UNREACHABLE objects, an accidentally-retained reference prevents collection indefinitely, growing memory usage over time.
public class EventPublisher {
public static event Action? OnSomethingHappened; // static event: a common leak source
}
public class Subscriber {
public Subscriber() {
EventPublisher.OnSomethingHappened += HandleEvent; // if never unsubscribed, 'this' leaks forever
}
private void HandleEvent() { }
}
Real-world example Debugging a long-running service whose memory usage climbs steadily, traced back to event subscriptions that were never unsubscribed.
Common follow-ups: Why are STATIC event subscriptions specifically a particularly common and dangerous source of memory leaks?
Delegates Events & Lambdas