Java Garbage Collection

In the last lesson you learned about Java stack memory. We kept mentioning the thing that frees heap memory. Here it is: Java cleans up unused memory for you with garbage collection.

🤔 The problem garbage collection solves

Think of a table where you keep stacking plates. Nobody clears them, so the table fills up and there is no room for new ones. Your program’s memory works the same way.

  • Every object you make with new takes up heap memory (the area where Java keeps objects).
  • If that memory is never freed, the program slowly uses more and more.
  • Then it runs out and crashes. So someone has to clean up.

In older languages like C and C++, that someone is you. Here is the pain:

  • You free each object by hand when you finish with it.
  • Free it too early, while other code still uses it, and the program crashes.
  • Forget to free it, and that memory is lost for the rest of the run. That’s a memory leak.

Java takes this whole burden away.

  • The garbage collector is an automatic cleanup crew built into the JVM.
  • It finds objects you no longer use and frees them for you.
  • You just create objects and stop using them. The JVM notices and cleans up.
  • So you can never free too early, and you rarely forget.

🧩 What is garbage collection?

Garbage collection (GC) is the JVM finding objects you no longer need and freeing them. So how does it decide?

  • The JVM runs it in the background while your program runs.
  • You never start it, schedule it, or write code for it. It just happens.
  • It asks one question about every object: can the program still get to this?
  • Yes, the object stays. No, it’s garbage and gets freed.

Here is the simplest object becoming garbage.

Student s = new Student(); // the object is reachable through s
s = null; // ✅ now nothing refers to it: it is garbage

Line by line:

  • We make a Student and point s at it. While s points to it, it stays alive.
  • We set s to null, so nothing points to it.
  • It’s garbage now. The collector frees it next time it runs.

See, you never freed it yourself.

🔗 What “reachable” really means

Reachable is the heart of everything. An object is reachable if the program can arrive at it by following references from a set of starting points.

Those starting points are GC roots — references the program always has direct access to. The collector treats roots as “definitely alive” and works outward.

The common roots:

  • Local variables on the stack of any running method.
  • Static fields, which live as long as the class is loaded.
  • Active threads, since a running thread is always alive.

So the rule is simple:

  • The collector starts at every root and follows every reference it can find.
  • Root to object, then that object to the objects it references, and so on.
  • Everything it can touch is reachable, so it stays.
  • Everything it can’t touch is unreachable, so it’s garbage.

Notice it doesn’t matter if the object still references other objects. A whole island of objects pointing at each other is still garbage, as long as nothing from a root points into that island.

class Node {
Node next; // a Node can point to another Node
}
Node a = new Node(); // a is a local variable, so it is a GC root reference
a.next = new Node(); // the second Node is reachable only through a.next
a = null; // ✅ now BOTH Node objects are unreachable
  • a becomes null, so no root reaches the first Node.
  • The second Node was reachable only through a.next, so it’s gone too.
  • They still reference each other, but nothing from a root gets in. Both are garbage.

So Java is never confused by objects pointing at each other in a circle, unlike older reference-counting systems.

🪄 How to make an object eligible for collection

You never delete an object in Java. There is no free, no delete. You make an object eligible (the collector is allowed to reclaim it) by making it unreachable. A few everyday ways:

  • Set the reference to null.
  • Reassign the reference to a different object, dropping the old one.
  • Let the reference go out of scope when a method finishes.

Let’s see reassignment, the most common one.

public class Main {
public static void main(String[] args) {
String message = new String("Hello");
System.out.println(message);
message = new String("World"); // ✅ the old "Hello" is now unreachable
System.out.println(message);
// the "Hello" object can now be garbage collected
}
}

The flow:

  • We point message at a “Hello” object.
  • We point the same variable at a new “World” object.
  • Now nothing refers to “Hello”, so it’s eligible for collection.
  • The JVM frees it next time the collector runs.

So reassigning the variable made the old object garbage on its own. No hand-freeing.

Output

Hello
World

Going out of scope works the same way, with no null at all. When a method returns, its locals vanish, so any object reachable only through them becomes garbage.

public class Main {
static void greet() {
String name = new String("Alex"); // reachable through name
System.out.println(name);
} // ✅ greet ends, name disappears, the "Alex" object is now garbage
public static void main(String[] args) {
greet();
// the "Alex" object created inside greet is unreachable here
}
}

When greet finishes, name is gone and nothing else points to “Alex”, so it’s eligible for collection. This is the most natural way objects die in Java, and you get it for free by writing normal methods.

🧬 Generations: where objects live in the heap

Here is the clever part. The collector is built around one fact: most objects die young. Think temporary strings, small helpers, values inside a loop, created and thrown away in a moment.

So the JVM splits the heap into generations. Picture two boxes: a small one emptied often, a larger one emptied rarely.

  • The young generation holds brand new objects. Collected often and fast, because most die quick.
  • The old generation (also called tenured) holds objects that survived several collections. Collected far less often.

The young generation is split further:

  • Eden is where every new object is born.
  • Two survivor spaces hold objects that lived through a collection, giving them another chance to die before promotion.

The life of an object:

  • Born in Eden.
  • Eden fills up, a young collection runs and clears the dead.
  • The survivors move into a survivor space, and their age goes up.
  • Survive enough times and the JVM promotes it to the old generation.

♻️ Minor, major, and full collections

Because the heap has these areas, there are different sizes of cleanup.

  • A minor GC cleans only the young generation. Runs often, finishes fast.
  • A major GC cleans the old generation. Runs less often, takes longer.
  • A full GC cleans the whole heap. The heaviest, and it can pause the program briefly, so you want it rarely.

This is efficient: most effort goes to the young generation, where most garbage is. And you manage none of it. The JVM sizes the generations, times the collections, and promotes objects itself.

🧹 Mark and sweep, in plain words

Most GC algorithms share one core idea, called mark and sweep. It works in two passes.

  • Mark. Start at the roots, walk every reachable reference, mark each object “alive”. Anything left unmarked is garbage.
  • Sweep. Go through the heap and reclaim the memory of every unmarked object.

Real collectors add steps like compacting survivors to remove gaps, but that’s the core. The takeaway: the collector finds garbage by reachability, not by how long ago you used something. An object is kept because it can be reached, full stop.

🚫 Do not call the garbage collector yourself

You might see System.gc() in some code. It requests a collection, but you should almost never call it. The JVM times collections far better than you can.

System.gc(); // ❌ a REQUEST, not a command; usually a bad idea to call

Why it’s trouble:

  • The JVM is allowed to ignore it, and often does.
  • When it listens, it may run a full collection at a bad moment and pause your program.
  • So it does nothing or hurts performance.

The right way to help: just stop referencing objects you’re done with. They become unreachable on their own.

One more old feature to avoid: the finalize() method that the collector called before freeing an object. It’s deprecated.

  • No guarantee it ever runs.
  • No guarantee when it runs.
  • It can slow collection down.

So to release a file or network connection, use a try-with-resources block or close it explicitly instead.

Memory leaks can still happen

Garbage collection frees unreachable objects, but it cannot free objects you are still referencing by mistake. If you keep adding objects to a long-lived collection and never remove them, they stay reachable and pile up. That is a memory leak, even in Java. Remove references you no longer need.

💧 How leaks still happen in Java

Java can leak memory, and the cause is always the same.

  • You hold a reference you no longer need.
  • So a reachable object never becomes garbage.
  • The collector is doing its job fine. The object just isn’t garbage, because you’re still pointing at it.

The classic example is a growing static collection. A static field is a GC root, so anything inside it stays reachable as long as the class is loaded, usually the whole program.

public class Cache {
// ❌ static list lives for the whole program; it is a GC root
static List<byte[]> entries = new ArrayList<>();
static void add(byte[] data) {
entries.add(data); // ❌ we add but never remove: this grows forever
}
}
  • Every chunk we add stays reachable through that static list, so the collector can’t free it.
  • Call add in a loop and the heap fills until you run out of memory.
  • The collector is blameless. A live reference told it we still want every object.

The fix is to stop referencing what you’re done with.

public class Cache {
static List<byte[]> entries = new ArrayList<>();
static void add(byte[] data) {
entries.add(data);
}
static void doneWith(byte[] data) {
entries.remove(data); // ✅ drop the reference so it can be collected
}
}

Once an item is removed, nothing reaches it, so it’s eligible for collection like any other garbage. So in Java you don’t manage memory by freeing it. You manage it by being careful which references you keep alive.

⚠️ Common Mistakes

A few garbage collection misunderstandings to clear up.

  • Thinking you must free memory manually. In Java you do not. The collector frees unreachable objects on its own. There is no free or delete.

  • Calling System.gc() to “help”.

System.gc(); // ❌ a hint the JVM can ignore; may cause a badly timed pause
// ✅ instead, just stop referencing objects you are done with
  • Assuming GC prevents all leaks. It only frees unreachable objects. Objects you still reference stay alive.
static List<Object> all = new ArrayList<>(); // ❌ static root keeps everything
all.add(thing); // grows forever, never collected
  • Relying on finalize() for cleanup. It is deprecated and may never run. Use try-with-resources to close files and connections instead.

✅ Best Practices

Habits and takeaways about memory.

  • Let the JVM manage memory. Create objects freely and stop referencing them when you are done. The collector handles the rest.
  • Do not call System.gc(). Trust the automatic collector to choose the right moment.
  • Remove references you no longer need. Clearing entries from long-lived collections, especially static ones, prevents leaks.
  • Prefer the narrowest scope. A local variable that goes out of scope at the end of a method makes its object collectable for free.
  • Use try-with-resources, not finalize(), to release files, streams, and connections.

🧩 What You’ve Learned

Nicely done. Let’s recap garbage collection.

  • Garbage collection automatically frees heap memory for objects you no longer use, so you do not free it by hand like in C or C++.
  • ✅ An object is reachable if the program can get to it from a GC root such as a stack local or static field; unreachable objects are garbage.
  • ✅ You make an object eligible by setting its reference to null, reassigning it, or letting it go out of scope.
  • ✅ The heap is split into a young generation (Eden plus survivor spaces) and an old generation; most objects die young, so minor GC runs often and cheaply.
  • ✅ The collector finds garbage with mark and sweep, starting at roots and reclaiming everything unmarked.
  • ✅ Do not call System.gc() or rely on the deprecated finalize(); and remember that holding references too long still causes memory leaks.

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What does the garbage collector do?

    Why: The garbage collector reclaims memory from objects nothing can reach anymore.

  2. 2

    When does an object become eligible for garbage collection?

    Why: An object is collectable once it is unreachable, with no references pointing to it from any GC root.

  3. 3

    Why is the heap split into young and old generations?

    Why: Most objects are short-lived, so collecting the young generation frequently with a minor GC is efficient.

  4. 4

    Should you call System.gc() to free memory?

    Why: System.gc() is a suggestion the JVM may ignore; let the JVM manage collection automatically.

🚀 What’s Next?

You now know how the JVM runs your code and manages memory. Next we start writing real Java, beginning with variables, the named boxes that hold your data.

Java Variables

Share & Connect