The Java Memory Model & Garbage Collection

Course 2 · Ch 6
The Java Memory Model & Garbage Collection
Closing the loop rust1-1 opened on day one of this entire multi-language arc

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

Object obj = new Object(); // reachable — a stack-held reference points to it obj = null; // now unreachable — nothing points to the original object anymore

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

static List<Object> cache = new ArrayList<>(); // a static field — always reachable from a GC root void process(Object data) { cache.add(data); // every object added here is now reachable forever — never eligible for collection }

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.

ApproachC (c2-2)C++ (cpp1-5)JavaRust (rust1-1)
Who frees memorythe programmer, manuallydestructors, on scope exitthe garbage collectorthe compiler, via ownership rules
Timingwhenever free() is calleddeterministic — scope exitnon-deterministicdeterministic — scope exit
Use-after-free possibleyesyes, if misusedno — structurally impossibleno — compile-time error
Runtime overhead for safetynoneminimalreal — pause times, bookkeepingnone — checked at compile time
System.gc() is a request, not a command
Calling 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.
An ever-growing static collection is a real, classic Java memory leak
The GC can only answer "is this object reachable?" — never "is this object still logically needed?" A cache or list that keeps accumulating references with nothing ever removed will grow forever, entirely reachable, entirely un-collectible, and entirely a bug.

Coding Challenges

Challenge 1

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 solution
Challenge 2

Write 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 solution
Challenge 3

Write 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 solution

Chapter 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