Back to the map

Module 21 · Types and memory · Lesson 2 of 4

Object lifetime and ownership

Who decides when an object dies. Manual freeing, RAII and smart pointers, Rust's ownership rules, reference counting and its cycles, and tracing garbage collection.

CoreShould-knowMid 12 min Updated Nov 2026

Why it matters

Every object is born, used and must eventually be destroyed so its memory, file handle or connection can be reused. Get it wrong one way and you crash with use-after-free or double-free bugs, historically the largest source of security vulnerabilities in C and C++. Get it wrong the other way and memory or connections leak until the service falls over. Each language picks a strategy, and knowing which one you are in tells you which bugs to watch for.

The idea

Ownership is the question of who is responsible for ending an object's life. The strategies trade programmer effort, runtime cost and predictability differently.

StrategyWho freesWhenLanguagesMain risk
ManualThe programmer, explicitlyWhen free or delete is calledC, older C++Use-after-free, double free, leaks
Scoped ownership (RAII)The owning object's destructorDeterministically, when the owner goes out of scopeC++ (unique_ptr), RustCycles with shared ownership; complexity of lifetimes
Compile-time ownershipThe compiler inserts the freeWhen the single owner goes out of scope; borrows checked at compile timeRustLearning curve; some structures (graphs) need workarounds
Reference countingThe runtime, when the count reaches zeroImmediately when the last reference goesSwift, Objective-C, Python (plus a cycle collector), C++ shared_ptrCycles never reach zero; counting overhead
Tracing garbage collectionThe collectorSome time after the object becomes unreachableJava, Go, C#, JavaScriptPauses and memory overhead; leaks via lingering references

Tracing garbage collection in one picture

A tracing collector starts from roots (stack variables, static fields, registers), follows every reference, marks what it can reach, and reclaims everything else. Unreachable objects are garbage even if they reference each other, so cycles are not a problem. Modern collectors are generational (most objects die young, so a small young area is collected often and cheaply) and largely concurrent (marking happens while the program runs), keeping pauses to milliseconds or less for most services.

stackstaticsOrderCacheItemABunreachable → collected
The collector traces from roots. Everything reachable survives; the unreachable cycle on the right is collected even though its objects point at each other.

Rust's rules, briefly

Every value has exactly one owner; when the owner goes out of scope, the value is dropped. You can lend it out as any number of read-only borrows or exactly one mutable borrow at a time, and borrows cannot outlive the owner. The compiler checks all of this, so use-after-free and data races are compile errors, and no garbage collector is needed. Shared ownership, when genuinely needed, is opt-in through reference-counted types.

Memory is not the only resource

Files, sockets, database connections, locks and temporary directories must be released promptly and deterministically, and a garbage collector will not do that for you, because it runs whenever it likes, or never. Finalizers that run "eventually" are unreliable and deprecated in Java. Use the language's scoped construct so release happens on every path, including exceptions:

try (var conn = pool.getConnection();
     var stmt = conn.prepareStatement(sql)) {
    return stmt.executeQuery().next();
}   // stmt and conn are closed here, in reverse order, even if an exception is thrown

Python has with, C# has using, Go has defer, C++ and Rust release in destructors.

Leaks in garbage-collected code

A GC frees what is unreachable, so a leak in Java or JavaScript is an object that is still reachable but no longer useful. The usual suspects: unbounded static caches and maps that only grow; listeners and callbacks registered but never removed; thread-local values in pooled threads; collections holding references to completed requests; closures capturing large objects. Bound caches with eviction, unregister listeners, and use weak references where an entry should not keep its key alive.

Trade-offs

Manual management is fastest and most dangerous. RAII and Rust ownership give deterministic, low-overhead cleanup with more design effort up front. Reference counting frees promptly but leaks cycles and costs a counter update on every copy. Tracing GC removes whole classes of bugs and makes most code simpler, at the cost of memory headroom, background CPU and occasional pauses, and it never manages non-memory resources for you.

In an interview

Show you know what the runtime does and does not handle: "The service is on the JVM, so memory is collected, but connections come from a pool and are always returned in try-with-resources. The per-tenant cache is bounded with LRU eviction, because an unbounded map is how GC languages leak." For latency-sensitive systems, mention GC pause tuning or why Rust or C++ was chosen.

Check yourself

A Java service's heap grows steadily for days until it is killed for running out of memory. A heap dump shows millions of entries in a static HashMap used to cache user sessions. Why did the garbage collector not free them?

Object lifetime and ownership · Backplane