Skip to content
Ameen Abozaid
PublishedBEG-038-L03

Garbage collection, gently

  • Lesson 3 of 5
  • 10 min read
  • Beginner
On This Lesson 11 topics

Lesson Overview

In languages such as C you must free memory yourself. In Java you create objects with new and never write a matching "free". The JVM has a part called the garbage collector (GC) that finds objects the program can no longer use and reclaims the memory they occupy.

This lesson explains, at a high level, when an object counts as garbage, what you can and cannot control, and why you can still waste memory even though Java cleans up for you. We deliberately skip how collectors work internally and how to tune them; those topics come in later levels.

You will use the stack-and-heap drawings from lesson 1 and the aliasing idea from lesson 2: an object is garbage only when every arrow to it has gone.

Learning Objectives

By the end of this lesson you will be able to:

  • Explain what garbage collection is and why Java programs do not free objects manually.
  • State when an object becomes eligible for garbage collection, using the word "reachable".
  • Point to the moment of eligibility in a short code snippet.
  • Explain why System.gc() does not free memory on demand.
  • Explain how a program can still run out of memory through references it forgot to drop (a memory leak).
  • Tell eligibility from actual collection.

Written Explanation

What the garbage collector does

The heap fills up as your program creates objects. Without cleanup it would eventually run out. The garbage collector periodically looks at the heap, finds objects that the running program can no longer use, and makes their space available again. You never call free, and you never "delete" an object.

Reachable and unreachable

The key word is reachable. The Java Language Specification defines a reachable object as one that can be accessed in any potential continuing computation from some live thread. In plain terms: start from what the running code can touch directly (local variables in the frames of the stack, plus static fields), follow the arrows, and mark every object you can get to. Those are reachable. Everything else is unreachable.

An unreachable object is eligible for garbage collection. Nothing in the program can ever use it again, so reclaiming it is safe.

text
STACK                     HEAP
main
  p [ ●-----> Person("second")        <- reachable (arrow from the stack)
  q [ null ]
                          Person("first")   <- no arrow reaches it: eligible

Three details matter:

  1. Reachability is about arrows from running code, not about how many arrows. Two unused objects that point at each other are still garbage if nothing outside them points in. (Some other memory-management schemes count references and struggle with this, but Java's reachability check does not.)
  2. What removes the last arrow:
    • the variable is reassigned to another object (p = new Person();),
    • the variable is set to null,
    • the method returns and its frame disappears,
    • the object holding the reference becomes unreachable itself (for example a list that is no longer referenced makes all of its elements candidates).
  3. Eligible is not the same as collected. After the last arrow is gone, the object may stay in memory for a while. The JVM decides when to collect.

You do not control when it runs

System.gc() exists, but its Javadoc says only that it suggests that the JVM expend effort toward recycling unused objects, and that a best effort has been made when the call returns. It is a hint, not a command. Which objects are freed, and how much memory comes back, is up to the JVM. You should never write a program whose correctness depends on System.gc(), and you should not scatter it through code hoping to "free memory". The collector already runs automatically when it needs to.

For the same reason, setting a variable to null is not a general habit. A local variable ends with its method anyway. You use null or remove only for long-lived references (see the next section).

Memory leaks in a garbage-collected language

If Java cleans up unreachable objects, how can a program leak memory? By keeping objects reachable that you will never use again. The classic example is a long-lived collection that only grows: a static list, a cache Map, or a "history" that nobody trims. Everything inside is reachable from the static field, so the collector must keep it.

The collector can only free what is unreachable. Whether something is useful is a question only you can answer. Leaks are a design problem: decide who owns data, and remove entries that are no longer needed. Finding leaks with tools is a later topic (a "leak diagnosis" skill that you will meet in the JVM modules of the roadmap).

The cost of large lifetimes

Short-lived objects, such as a local Report created inside a method, are cheap: they become unreachable as soon as the method returns. Objects held in static fields or long-lived collections stay for the life of the program. That is why you should keep data in the smallest scope that works. It is also why "make everything static" (a mistake from BEG-020) has a memory cost.

Examples

Example 1: when does the first object become eligible? (simple)

java
Person p = new Person("first");
Person q = p;
p = new Person("second");   // first still reachable via q
System.out.println(q.n);
q = null;                   // now first is unreachable
System.out.println(p.n);

Output:

text
first
second

Trace the arrows to the "first" object:

After lineArrows to "first"Eligible?
Person p = new Person("first");pNo
Person q = p;p, qNo
p = new Person("second");qNo
q = null;noneYes

Note that q.n printed first after p was reassigned: the object survived because q still pointed to it. The eligibility is only at the last line.

Example 2: a local object in a method (practical)

java
static int work() {
    Report r = new Report();      // local; unreachable after return
    return r.data.length;
}
public static void main(String[] args) {
    System.out.println(work());
    System.gc(); // only a hint
    System.out.println("done");
}

(Report holds a byte[1000] array.) Output:

text
1000
done

r lives in work's frame. When work returns, the frame is removed and the Report has no arrow, so it is eligible, and so is its array. The program prints done whether or not the JVM collects anything at that moment. We could not see the memory being freed, and the program does not need to. That is the point: you are not supposed to observe it.

Example 3: a leak made of reachable objects (realistic)

java
static final List<byte[]> HISTORY = new ArrayList<>();
static void handle(int i) {
    byte[] payload = new byte[1024];
    HISTORY.add(payload);  // never removed
}
// main: for (int i = 0; i < 5000; i++) handle(i);
System.out.println("kept " + HISTORY.size() + " payloads reachable");

Output:

text
kept 5000 payloads reachable

Each call's local payload variable disappears when handle returns, but the array does not: the static HISTORY list still points to it. All 5,000 arrays are reachable from a static field, so none are garbage. This is the same code path that, with enough requests, would eventually exhaust the heap (lesson 4). The fix is a design decision: do not keep the payload, keep only what you need (an id or count), or remove old entries.

Backend Insight

A backend process runs for days or weeks, handling millions of requests. Garbage collection is what keeps it healthy: per-request objects die young and are collected cheaply. The risks are therefore:

  • Unbounded caches and registries. A Map that stores a result for every user id and never evicts is a leak. Production systems use caches with a maximum size or expiry.
  • Listeners and callbacks that are never unregistered. A long-lived object holding a reference to a short-lived one keeps it alive.
  • Large result sets loaded into memory. A query that returns 10 million rows into a list puts them all on the heap at once; they are all reachable until you finish with them.

Also, because collection timing is not under your control, you should not rely on it to release non-memory resources such as files or database connections. That is the job of try-with-resources (BEG-029).

In the roadmap, JVM memory management (MID-002) and leak diagnosis (MID-008) pick up here. For now, the mental model is enough: GC frees the unreachable, and you are responsible for not keeping things reachable.

Common Mistakes

Mistake 1: believing System.gc() frees memory now

What goes wrong: you call it in a loop, or after "big" operations, expecting memory to drop.

Fix: treat it as a hint only. If memory is a problem, find what is keeping objects reachable.

Mistake 2: setting every variable to null "to help GC"

What goes wrong: code clutter, and in locals it does nothing, since the frame ends anyway.

Fix: keep variables in a small scope. Only clear long-lived references (a field, a cache entry) when you are done with them.

Mistake 3: confusing "not used any more" with "not reachable"

What goes wrong: you think an object you stopped using is freed, but a list or field still refers to it.

Fix: ask "who still holds an arrow to this?" The collector reasons about arrows, not intent.

Mistake 4: assuming eligible means gone

What goes wrong: you expect an object to disappear on the very line where its last reference was dropped.

Fix: eligibility is the earliest moment it can be collected; when it happens is the JVM's choice.

Exercises

Level A: Foundation

  1. In Person p = new Person("a"); p = null;, on which line does the object become eligible?
  2. Fill in the blank: an object is eligible for garbage collection when it is no longer ______.
  3. True or false: calling System.gc() guarantees the memory is freed before the next line runs.

Level B: Application

  1. After Person a = new Person("x"); Person b = a; a = null;, is the Person eligible? Justify with arrows.
  2. A method builds List<Person> list = new ArrayList<>();, adds three persons, and returns. Nothing else holds the list. Are the three persons eligible after the return? Explain.
  3. The class below has a memory problem. Name it and propose a fix.
java
class Sessions {
    private static final Map<String, Session> ALL = new HashMap<>();
    static void open(String id) { ALL.put(id, new Session(id)); }
}

Level C: Challenge

  1. For this code, say how many Person objects are reachable at each marked point:
java
Person[] team = new Person[3];
team[0] = new Person("A");
team[1] = new Person("B");
// point 1
team[0] = null;
// point 2
team = null;
// point 3

Then explain why a pair of Person objects that reference each other is still garbage after team = null if nobody else points to them.

Show solutions
  1. On the second line, p = null;: that removes the only arrow.
  2. Reachable.
  3. False. The Javadoc describes it as a suggestion with best-effort recycling; nothing is promised about when or how much memory is freed.
  4. No. b still points to the object. It becomes eligible only when b is also reassigned or set to null (or b goes out of scope).
  5. Yes, assuming no other references. The list became unreachable when the frame disappeared, and the persons were reachable only through the list.
  6. A static map that only grows: every session stays reachable, so none is ever collected. Fix: remove entries when sessions close (add a close(String id) that calls ALL.remove(id)), or use a bounded cache.
  7. Point 1: 2 (A and B). Point 2: 1 (only B, since the array still holds B). Point 3: 0, because the array itself is unreachable and so are its elements. For two objects that reference each other: reachability starts from the stack and static fields, so if no such path leads to either object, both are unreachable even though they point to one another.

Mini Challenge

Take the HISTORY example from this lesson and rewrite handle so that memory use stays bounded: keep at most the last 100 payloads. Write the idea in two or three lines of code or pseudocode (for example removing the oldest element when the list reaches 100) and add one sentence stating which objects become eligible for collection as a result.

Summary & Next Lesson

Key takeaways

  • Java frees heap objects automatically; this is garbage collection.
  • An object is eligible when it is unreachable from running code, regardless of how many unreachable objects point to each other.
  • The last arrow disappears when you reassign, set null, return from the method, or drop the owner.
  • System.gc() is only a hint; you cannot choose when collection happens.
  • Leaks happen when you keep objects reachable that you will never use again.

Next lesson

Next is OutOfMemoryError and StackOverflowError: the two errors you will see when the heap or the stack runs out.

Self-check

  1. What does "reachable" mean?
  2. Name three ways the last reference to an object can disappear.
  3. Does eligibility mean the object is freed immediately?
  4. Why can a static List that only grows cause trouble even though Java has garbage collection?
  5. What does System.gc() actually promise?
Show answers
  1. The program can still get to the object by following references from running code (locals on the stack, static fields).
  2. Reassign the variable, set it to null, return from the method whose frame held it (also: the owning object becomes unreachable).
  3. No. It means the object may be collected; the JVM decides when.
  4. Everything inside is reachable through the static field, so the collector must keep it all.
  5. Only a suggestion with a best effort to recycle unused objects; no guarantee of what or when.

References

  • java.lang.System (Java SE 8 API): verified that System.gc() suggests the JVM expend effort toward recycling unused objects, with a best effort when the call returns; this is the basis for calling it a hint. I read the Java 8 page; the behaviour description is long-standing but I did not re-check it in the Java 21 page.
  • Java Language Specification, section 12.6.1: verified that a reachable object is one accessible in any potential continuing computation from a live thread. The "eligible" wording and the point about objects that reference each other are my own plain-language reading of that definition.
  • Code examples were compiled and run with JDK 21 and the output copied. The claim that a collected object's memory use cannot be seen from the program is a deliberate simplification.

Practice

Apply this lesson with the module's lab, exercises and quiz.

Lab and exercises Quiz

Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.

Continue to next lesson

Type to search levels, phases, modules and lessons.