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.
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.
| Strategy | Who frees | When | Languages | Main risk |
|---|---|---|---|---|
| Manual | The programmer, explicitly | When free or delete is called | C, older C++ | Use-after-free, double free, leaks |
| Scoped ownership (RAII) | The owning object's destructor | Deterministically, when the owner goes out of scope | C++ (unique_ptr), Rust | Cycles with shared ownership; complexity of lifetimes |
| Compile-time ownership | The compiler inserts the free | When the single owner goes out of scope; borrows checked at compile time | Rust | Learning curve; some structures (graphs) need workarounds |
| Reference counting | The runtime, when the count reaches zero | Immediately when the last reference goes | Swift, Objective-C, Python (plus a cycle collector), C++ shared_ptr | Cycles never reach zero; counting overhead |
| Tracing garbage collection | The collector | Some time after the object becomes unreachable | Java, Go, C#, JavaScript | Pauses 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.
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?