The Java Memory Model & Garbage Collection
Every chapter since java1-2 has created objects freely, never once deallocating one. This chapter explains why that was always safe to do — and revisits a promise this site made all the way back at the very start of the Rust track.
The Heap & Stack, Revisited
java1-2 established that primitives live directly on the stack (or inline in an object) while everything created with new lives on the heap, accessed only through a reference. This chapter is about that heap side specifically — who cleans it up, and how.
Automatic Memory Management — No free(), No delete
Java simply never requires manual deallocation of a heap object. There is no free() like c2-2's own manual malloc/free discipline, and no destructor-driven RAII like cpp1-5's scope-exit cleanup. Once an object is created, its lifetime is entirely the JVM's problem, not the programmer's.
Reachability — How the JVM Decides What to Free
An object becomes eligible for collection once it's unreachable — no live reference chain from a GC root (stack references, active static fields, and similar) reaches it anymore. This is real reachability-graph analysis, not simple reference counting — it correctly handles cases like two objects only referencing each other, with neither reachable from anywhere else.
Generational Garbage Collection, Briefly
Most objects die young — the "weak generational hypothesis" the JVM is built around. New objects are allocated in a small young generation, collected frequently and cheaply (a minor GC); objects that survive several collections get promoted to an older generation, collected less often but more expensively (a major/full GC). This chapter stays at that level of detail — the specific algorithms are a genuinely deep topic of their own.
Closing rust1-1's Loop
Right at the start of this site's entire multi-language arc, rust1-1 named Rust's founding goal explicitly: memory safety without a garbage collector. Every language covered since — C's manual malloc/free (c2-2), C++'s RAII (cpp1-5) — gave deterministic, immediate cleanup, but only by trusting the programmer to get it right; c1-7/c2-2/c2-3's entire catalog of dangling pointers, use-after-free, and double-free bugs are the cost of that trust being misplaced. Rust refused the garbage-collector tradeoff and instead proved those same bugs impossible at compile time, through ownership and borrowing.
Java is the site's first language to go the other direction entirely — fully accepting the tradeoff Rust was built specifically to refuse. In exchange for real, concrete costs (unpredictable pause times, memory overhead for the collector's own bookkeeping, no deterministic object destruction moment), an entire category of memory bugs becomes structurally impossible rather than merely discouraged: an object can never be freed while a live reference to it still exists, so use-after-free and double-free simply cannot happen in ordinary Java code at all.
What GC Does NOT Prevent
Memory leaks are still genuinely possible in Java. If a reference is unintentionally kept alive — most classically, an ever-growing static collection — every object it holds remains reachable, and the GC has no way to know that reachable memory is logically no longer needed. This is a real, different kind of leak than C's dangling-pointer-adjacent leaks, but a leak all the same.
| Approach | C (c2-2) | C++ (cpp1-5) | Java | Rust (rust1-1) |
|---|---|---|---|---|
| Who frees memory | the programmer, manually | destructors, on scope exit | the garbage collector | the compiler, via ownership rules |
| Timing | whenever free() is called | deterministic — scope exit | non-deterministic | deterministic — scope exit |
| Use-after-free possible | yes | yes, if misused | no — structurally impossible | no — compile-time error |
| Runtime overhead for safety | none | minimal | real — pause times, bookkeeping | none — checked at compile time |
System.gc() only hints to the JVM that now might be a good time to collect — it doesn't force an immediate collection. The JVM decides when collection actually runs; relying on this call for correctness rather than pure diagnostics is a mistake.
Coding Challenges
Write code that creates an object, assigns null to its only reference, and explain in a comment exactly why that object is now eligible for collection, using the term "reachability."
📄 View solutionWrite a class with a static List field that a method repeatedly adds objects to but never removes from, and explain in a comment why this is a genuine memory leak despite Java having a garbage collector.
📄 View solutionWrite a short comparison, in comment form, contrasting how a use-after-free bug from c1-7/c2-2's own C material would be impossible to reproduce in ordinary Java code, tying your answer to this chapter's reachability model.
📄 View solutionChapter 6 Quick Reference
- Java requires no manual deallocation — no free(), no destructors, unlike c2-2's malloc/free or cpp1-5's RAII
- An object becomes collectible once unreachable from any GC root — real reachability-graph analysis, not reference counting
- Generational GC: most objects die young (minor GC), survivors get promoted to an older generation (major/full GC)
- Java fully accepts the GC tradeoff rust1-1 named Rust as being built specifically to refuse — real runtime cost, in exchange for use-after-free/double-free becoming structurally impossible
- GC does not prevent logical memory leaks — an ever-growing static collection remains reachable forever
- System.gc() is only a hint — the JVM decides when to actually collect
- Next chapter: records, sealed classes, and modern Java features — bridging back to Kotlin's own data classes and sealed classes