Garbage Collection in Java
Reading Time: 12 Minutes
Difficulty: Intermediate
Topic Summaryβ
Garbage Collection (GC) is Java's automatic memory management system. Instead of manually freeing memory like in C/C++, Java's GC automatically finds objects that are no longer used and reclaims their memory. Understanding how GC works helps you write memory-efficient code and avoid memory leaks.
What You'll Learnβ
- What makes an object eligible for garbage collection
- How GC roots work and how the GC traces live objects
- The Mark-and-Sweep algorithm explained simply
- Generational GC: Young Generation (Eden + Survivors), Old Generation, Metaspace
Prerequisitesβ
- JVM Architecture (Lesson 1)
- JVM Memory Areas (Lesson 3)
- Basic understanding of Java objects and references
Explanationβ
What is Garbage Collection?β
In languages like C and C++, programmers must manually allocate memory (malloc) and free it (free). Forgetting to free memory causes memory leaks β your program uses more and more memory until it crashes.
Java solves this automatically. The Garbage Collector (GC) runs in the background, identifies objects that are no longer reachable by your program, and frees their memory. You never call free() in Java.
Object is created β Object is used β Object becomes unreachable β GC frees memory
(new Foo()) ... (no references left) (automatically)
When is an Object Eligible for GC?β
An object becomes eligible for garbage collection when it is no longer reachable from any live thread. This means no variable, field, or data structure in your running program holds a reference to it.
public class GCDemo {
public static void main(String[] args) {
// Object created β NOT eligible for GC (referenced by 'obj')
Object obj = new Object();
// Now obj points elsewhere β original object has no references
obj = null; // original Object is NOW eligible for GC
// Another example:
StringBuilder sb = new StringBuilder("hello");
StringBuilder sb2 = sb; // two references to same object
sb = null; // still NOT eligible (sb2 still points to it)
sb2 = null; // NOW eligible (no references left)
System.out.println("Objects are eligible for GC now");
}
}
Cases that make an object eligible:
- Reference set to
null - Reference goes out of scope (method ends)
- Object only referenced by other eligible objects (island of isolation)
- Re-assigning the reference variable to a different object
GC Roots β Where Tracing Beginsβ
The GC doesn't start from nothing. It starts from GC Roots β known starting points that are always considered "live". Any object reachable from a GC root (directly or through a chain of references) is considered alive and will NOT be collected.
GC Roots are:
- Local variables in active method Stack Frames
- Static variables in loaded classes
- Active threads themselves
- JNI references (native code references)
- Objects in synchronized monitors
GC Root (local variable 'list')
β
ArrayList object (alive β reachable)
β
String "Hello" (alive β reachable through list)
(Some other object with no path to any GC Root) β ELIGIBLE FOR GC
The Mark-and-Sweep Algorithmβ
The basic GC algorithm used (in various enhanced forms) is Mark-and-Sweep. It runs in two phases:
Phase 1: Markβ
Starting from all GC Roots, the GC traverses every reference chain and marks every reachable object as "alive".
GC Root β Object A (MARKED) β Object B (MARKED) β Object C (MARKED)
β Object D (MARKED)
Object E (not reachable β NOT marked) β will be swept
Object F (not reachable β NOT marked) β will be swept
Phase 2: Sweepβ
All objects that were NOT marked are unreachable β the GC sweeps (frees) their memory.
Before sweep: [A][B][C][D][E][F] (E, F are unmarked)
After sweep: [A][B][C][D][ ][ ] (memory freed)
Modern GCs also include a Compact phase to eliminate memory fragmentation:
After compact: [A][B][C][D] (objects moved together)
System.gc() β A Request, Not a Commandβ
You can ask the GC to run with System.gc(), but the JVM is not obligated to honor it immediately (or at all).
// This is a SUGGESTION, not a guaranteed invocation
System.gc();
// Better: just let the JVM decide when to GC
// Don't call System.gc() in production code
Why not call it? GC pauses stop your application threads ("Stop-the-World"). Calling it at the wrong time can degrade performance significantly.
finalize() β Deprecated and Dangerousβ
Before Java 9, you could override finalize() to run cleanup code before an object was GC'd. This is now deprecated (removed in Java 18) because:
- Timing is unpredictable (no guarantee when finalize runs)
- Objects can be "resurrected" in finalize (putting themselves back in scope!)
- It causes performance problems and delays GC
// DON'T do this (deprecated since Java 9)
@Override
protected void finalize() throws Throwable {
System.out.println("About to be GC'd");
super.finalize();
}
// DO this instead β use try-with-resources or explicit close()
try (MyResource resource = new MyResource()) {
resource.use();
} // resource.close() called automatically
Generational Garbage Collectionβ
Modern JVMs use Generational GC β a strategy based on the observation:
Most objects die young. A temporary object created in a loop is likely unreachable within milliseconds, while a configuration object created at startup may live for hours.
So the Heap is divided into generations to collect short-lived objects quickly without scanning the whole heap every time.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β HEAP β
β β
β ββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββ
β β Young Generation β β Old Generation ββ
β β β β (Tenured Space) ββ
β β ββββββββββββ ββββββ ββββββ β β ββ
β β β Eden β β S0 β β S1 β β β Long-lived objects ββ
β β β Space β β β β β β β promoted from Young ββ
β β β β β β β β β β ββ
β β β New objs β β Surβ β Surβ β β GC: Major / Full GC ββ
β β β born hereβ β 0 β β 1 β β β (infrequent, slow) ββ
β β ββββββββββββ ββββββ ββββββ β β ββ
β β GC: Minor GC (fast, frequent) β β ββ
β ββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββ
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Metaspace (not part of Heap β native memory) β β
β β Stores: Class metadata, method bytecode β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Young Generation β Where Objects Are Bornβ
Eden Space: All new objects are allocated here.
When Eden fills up, a Minor GC runs:
- GC marks all live objects in Eden and Survivor spaces
- Live objects are copied to one Survivor space (e.g., S0)
- Eden is completely cleared (very fast!)
- Objects that survive many GC cycles get promoted to Old Generation
Survivor Spaces (S0 and S1):
- Two survivor spaces exist; at any time, one is active and one is empty.
- Surviving objects bounce between S0 and S1 on each Minor GC.
- An object's "age" increments each time it survives.
- When age reaches the tenuring threshold (default: 15), the object is promoted to Old Generation.
New object β Eden
Eden full? β Minor GC β live objects copied to S0
Next Minor GC β S0 survivors copied to S1 (age++)
Next Minor GC β S1 survivors copied to S0 (age++)
... age reaches 15 β promoted to Old Generation
Old Generation (Tenured Space)β
Holds long-lived objects that have survived many Minor GCs. Collected by a Major GC (or Full GC), which is less frequent but takes longer. A full GC collects both Young and Old generations.
Metaspace (Java 8+)β
Not technically part of the Heap. Stores class metadata. Collected when classes are unloaded (e.g., when a ClassLoader is GC'd). Grows dynamically in native memory.
Real-World Analogyβ
Think of the Heap like a hotel:
- Eden is the check-in desk β most guests (objects) check in here.
- Survivor spaces are short-stay rooms β guests who stay a day or two move here.
- Old Generation is the long-term suites β guests who've been there for months live here.
- The hotel housekeeper (GC) checks all rooms regularly (Minor GC for short-stay, Major GC for long-term) and cleans out rooms where guests have already left (no references).
Code Exampleβ
public class GCEligibilityDemo {
public static void main(String[] args) {
// --- Example 1: Null reference ---
Object obj = new Object();
obj = null; // eligible for GC
// --- Example 2: Out of scope ---
{
String temp = new String("Temporary");
// temp used here
}
// temp is out of scope β the String object is eligible for GC
// --- Example 3: Island of Isolation ---
Node a = new Node("A");
Node b = new Node("B");
a.next = b;
b.next = a; // circular reference
// Now remove all external references
a = null;
b = null;
// Both nodes reference each other but neither is reachable
// from any GC root β both are eligible for GC!
// --- Example 4: Watching object creation pressure ---
System.out.println("Creating many short-lived objects...");
for (int i = 0; i < 1_000_000; i++) {
// These String objects die immediately after loop body
String s = "Object-" + i;
// s goes out of scope β eligible for GC next iteration
}
System.out.println("Done! GC handled cleanup automatically.");
}
static class Node {
String value;
Node next;
Node(String value) { this.value = value; }
}
}
Outputβ
Creating many short-lived objects...
Done! GC handled cleanup automatically.
Common Mistakesβ
- β Mistake: Calling
System.gc()frequently in production code β β Fix: Let the JVM manage GC timing. Manual calls can cause unnecessary Stop-the-World pauses. - β Mistake: Relying on
finalize()for resource cleanup β β Fix: Usetry-with-resourcesand implementAutoCloseable/Closeablefor deterministic cleanup. - β Mistake: Thinking circular references cause memory leaks in Java β β Fix: Java's GC handles circular references. It leaks only when objects are still reachable from GC roots (e.g., stored in a static collection that's never cleared).
- β Mistake: Keeping references in static collections forever β β
Fix: Use
WeakReferenceorWeakHashMapfor caches, so GC can reclaim objects when memory is needed.
Best Practicesβ
- Nullify references you no longer need only if they are long-lived (e.g., fields in long-running objects). For local variables, going out of scope is sufficient.
- Avoid memory leaks by removing objects from static collections when done.
- Use
WeakReferenceorSoftReferencefor cache implementations. - Monitor GC behavior with
-verbose:gcor GC logging flags to understand your application's GC behavior. - Prefer object pooling (e.g., connection pools) for expensive objects rather than creating and discarding them repeatedly.
Interview Questionsβ
Q: How does the Garbage Collector determine which objects to collect?
A: The GC starts from GC Roots (active threads, local variables, static variables, JNI references) and traverses all object references. Objects reachable from GC roots are "live" and kept. Objects not reachable from any GC root are eligible for collection. This is called reachability analysis.
Q: What is the difference between Minor GC and Major GC?
A: Minor GC collects the Young Generation (Eden + Survivor spaces). It's fast and frequent because most objects die young. Major GC (or Full GC) collects the Old Generation (and possibly Young Generation too). It's slower and less frequent. A Full GC is the most expensive β it stops all application threads and collects the entire heap.
Q: What is an "island of isolation" in Java GC?
A: An island of isolation is a group of objects that reference each other in a cycle, but none of them are referenced from any GC root. Even though they reference each other, they are all unreachable from the program, so they are all eligible for GC. Java's GC correctly handles this β unlike reference-counting GCs.
Q: Why is finalize() deprecated?
A: finalize() has several problems: (1) No guarantee on when or if it runs; (2) Objects can "resurrect" themselves during finalization; (3) It causes GC overhead β finalizable objects require two GC cycles to be collected; (4) Exceptions in finalize() are silently ignored. Use try-with-resources and AutoCloseable instead.
Quick Revisionβ
β GC automatically frees memory for objects no longer reachable from GC Roots
β An object is eligible when no live code holds a reference to it
β Mark-and-Sweep: mark all reachable objects, then sweep (free) the unmarked ones
β Young Generation (Eden + S0 + S1) β Minor GC (fast, frequent)
β Old Generation β Major GC (slow, infrequent)
β System.gc() is a suggestion; finalize() is deprecated β avoid both
Related Topicsβ
- GC Algorithms (Lesson 5)
- JVM Performance Tuning (Lesson 6)
- JVM Memory Areas (Lesson 3)
Next Lessonβ
Lesson 5 β GC Algorithms: Serial, Parallel, G1, ZGC, and Shenandoah