HotShard
Java memory

Why Java Needs a Garbage Collector

Java frees an object once no code can reach it any more. Here's why it can't be any simpler than that.

You write new thousands of times a second and never once free anything, and yet the memory doesn't run out, because something is cleaning up behind you. This is what it throws away, how it knows that's safe, and why the obvious way to do it doesn't work.

~9 min read

Watch the whole topic · or read it below

Objects outlive the method that made them#

TL;DRthe 30-second version
  • Objects live on the heap, which has no built-in moment when an object is finished.
  • Freeing by hand fails because no code can know who else still holds a reference.
  • Java frees an object only when no chain of references reaches it from a GC root, such as a stack variable or a static field.
  • Counting references can't free cycles, so Java doesn't count. It follows references from the roots instead, which is called tracing.

A running Java program keeps its data in two places.

The stack. Every thread has its own, and each method call gets a block on that stack, called a frame, which holds that call's local variables. When the method returns, its frame is thrown away, so cleanup on the stack is automatic: it happens because the method ended.

The heap. This is one large area shared by every thread, and every new creates an object here.

Why not keep objects on the stack too? Because they often have to outlive the method that made them:

List<User> load() {
    List<User> users = new ArrayList<>();  // the list is created on the heap
    return users;                          // the list must survive after load() ends
}
The list must survive load()

The variable users lives in load()'s frame, but it doesn't hold the list itself. It holds a reference, which is the heap address where the list is stored. When load() returns, its frame is gone and so is the variable, but the list has to stay, because the caller is about to use it.

while load() runs                      after load() returns

STACK            HEAP                  STACK            HEAP
[load()  users ]──► ArrayList @0x1A40  (frame gone)     ArrayList @0x1A40
[main()  list  ]                       [main()  list ]──►      ▲ still needed
Before and after load() returns

So a frame ends at a known moment, when its method returns, but a heap object has no such moment. Nothing about the object itself tells you when it's finished, and that's the whole problem a garbage collector exists to solve.

Freeing by hand breaks in three ways#

The heap has a size limit (it can grow, but only up to a maximum), so if objects are never removed, it fills up and the program fails.

The obvious fix is to let the programmer free each object when they're done with it, which is how C works, with free(). But it goes wrong in three ways:

  1. Forget to free. The memory never comes back, and the heap slowly fills. That's a leak.
  2. Free too early. Some other code still holds the address. The memory gets reused for a new object, and the old code now reads and writes the new object's data. Often nothing crashes at that moment; the data is just silently wrong.
  3. Free twice. The memory manager's own records get corrupted.
1. code A ──► Order #17 ◄── code B
2. code A calls free(order)          code B still holds the address
3. a new object, User bob, is put in the same spot
4. code B reads order.total  ──►  gets bob's bytes. No crash, wrong data.
Free too early

All three have one cause. In a large program, no single piece of code can know whether some other code still holds a reference to an object.

So Java takes the decision away from you: you never free anything, and the runtime decides instead. The part that decides is the garbage collector, or GC.

Garbage means unreachable#

What should the GC free? The ideal rule is "free what the program will never use again". The trouble is, that would need the GC to predict the future, so it can't be built.

The rule Java uses instead is this: free what the program can no longer reach. If no chain of references leads to an object, no code can ever get its address again. Freeing it is provably safe.

Every chain has to start somewhere. It starts at a GC root: a reference the program can use directly, without first going through another object. The roots are:

  • local variables and parameters, in every frame of every thread's stack;
  • static fields of loaded classes;
  • a few references the JVM itself holds, for example objects held by native code, and objects currently used as locks.

An object is live if you can reach it from a root by following references. Everything else is garbage.

roots:   [local var a]          [static CACHE]
              |                       |
              v                       v
              A ----> B               C ----> D

              E ----> F        (nothing points to E)
Follow the arrows from the roots

Start at the roots and follow the arrows. From a you reach A and then B, and from CACHE you reach C and then D. Nothing leads to E, so E is garbage. And F? E points to F, but E is garbage, so that arrow doesn't count. F is garbage too.

That's the rule again, seen through the example. An object stays alive because something live points to it. What the object itself points to doesn't matter. E has a perfectly good arrow to F, and it saves neither of them.

PredictThe line a = null; runs. Which objects become garbage?

A and B. A's only path from a root was a. B's only path went through A. C and D are still reached from CACHE.

Java can still leak memory#

The rule is about reach, not use, and that has a consequence people often miss.

static Map<String, Session> sessions = new HashMap<>();

void login(String id) {
    sessions.put(id, new Session(id));   // never removed
}
A map that only grows

Every Session is reachable: from the static field sessions (a root), to the map, to the session. The user logged out an hour ago, and no code will ever look at that session again. But none of that matters to the GC. The session is reachable, so the GC is not allowed to free it. Each login adds one more, and the heap fills.

That's a memory leak in Java. The GC can't free memory you still point to. Common sources:

  • static collections that only grow;
  • caches with no eviction;
  • listeners that are registered and never removed;
  • ThreadLocal values on threads in a pool, which live as long as the thread does.

The rule again: reachable means alive, even when it's useless. The fix is always to cut the reference: remove the entry, add eviction, unregister the listener.

First try: count the references#

There's a simpler-sounding way to find unreachable objects. Give every object a counter: how many references point to it right now. Each new reference to the object adds 1, and each reference that goes away subtracts 1. When the count reaches 0, nothing points to it, so it can be freed.

A single assignment usually touches two counters, because it replaces a reference that was already there:

x = b;   // before this line, x pointed to a
  • b gains a reference, because x now points to it. b's count goes up by 1.
  • a loses one, because x doesn't point to it any more. a's count goes down by 1. If that reaches 0, a is freed, and everything a pointed to loses a reference too, which can free those objects in turn.

Two cases touch only one counter. If x was null before, only b changes. When a local variable disappears because its method returned, only the old object's count goes down.

But this idea has two problems: cycles, and cost.

   (no root points here)

   A [count 1]  ──►  B [count 1]
        ▲                 │
        └─────────────────┘
A cycle with no root

Cycles. A points to B, B points to A, and nothing else points to either. Each count is 1, from the other one, forever. Neither ever reaches 0, so neither is ever freed. Now apply the reachability rule: no root reaches A or B, so under that rule they're garbage, just like E and F. But counting can't see that, because it only looks at one object's number, never at the whole path back to a root. Cycles are everywhere: doubly linked lists, trees where each child points back to its parent, an object registered as its own listener.

What counting costs#

The second problem: even without cycles, counting is slow, because every reference write now does extra work on two counters. With several threads, two threads can change the same counter at the same moment, so every update must be atomic: done in one step another thread can't interrupt. Atomic updates are much slower than plain ones, and they now happen on every x = y in the program.

So Java traces instead#

That's why the standard JVM, HotSpot, doesn't count. It traces: it starts at the roots, follows every reference, and marks everything it reaches. Whatever isn't marked is garbage. A cycle with no path from a root never gets marked, so it's freed like anything else. And an assignment never has to update any counters.

Other languages chose differently, and each paid for it:

CountingTracing (Java)
When memory is freedthe moment the count hits 0later, when the GC runs
Cyclesleak, unless something extra handles themfreed
Cost lands onevery assignmentmostly the GC's own work (plus a small extra step on some reference writes, covered later), and sometimes a pause, when your threads stop while the GC works
Who uses itSwift (the programmer marks some references weak to break cycles); CPython (adds a separate cycle detector)Java, Go, C#, JavaScript engines

Tracing raises the next questions: how it finds everything quickly, how it turns garbage into free space once it knows what's garbage, and what happens to your program while it works. That's Part 2.

If this comes up in an interview#

The one-linerJava frees an object when it's no longer reachable from a GC root, meaning a stack variable, a static field, or a JVM-held reference. HotSpot finds this by tracing from the roots, not by counting references, because counting can't free cycles.
Does Java use reference counting?

No. Counting can't free cycles, and it adds atomic work to every reference write. Java traces from roots.

Can a Java program leak memory?

Yes. The GC frees what's unreachable, not what's unused. An object kept in a static map that only grows stays reachable forever.

What is a GC root?

A reference the program can use without going through another object: locals on thread stacks, static fields, and a few JVM-internal references.

Gotchas
  • "Setting references to null helps the GC." Usually not. A local variable goes away when its method returns anyway, and the JIT may treat it as dead even earlier. Clear a reference only when something long-lived would keep holding it, for example a slot in a long-lived array.
  • Finding a leak. Take a heap dump with jcmd <pid> GC.heap_dump heap.hprof. Open it in a heap analyser such as Eclipse MAT, and ask for the "path to GC roots" of the objects that keep growing. That path names the reference to cut.
  • WeakReference points to an object without keeping it alive. If weak references are the only way to reach an object, the GC may free it. WeakHashMap is built on this: it's useful for attaching extra data to objects you don't own, and an entry vanishes once its key is otherwise unreachable. Beware: a value that refers back to its own key keeps the entry alive forever.
  • finalize() is deprecated. There's no guarantee when it runs, or whether it runs at all. Release resources with try-with-resources.
Under the hood
  • The JVM's own internal tables, such as its list of loaded classes, are roots too.
  • Conceptually every new allocates on the heap, but when the JIT can prove an object never leaves its method, it may skip the heap allocation entirely. That's called escape analysis.
  • The JVM knows exactly which stack slots and fields hold references and which hold plain numbers. The compiler records this at the specific points where a thread can be stopped for GC. That precision is what makes tracing correct, and it matters even more in Part 2, where objects get moved.
References & further reading
References

Feedback on this topic →