By WebNest Studio Engineering Team
Java - Core Tutorial
Garbage Collection in Java
Garbage collection (GC) is the process by which the JVM automatically finds and reclaims heap memory occupied by objects that a running program can no longer reach through any chain of references. It is one of Java's defining features, and it is the direct reason Java has no explicit free() or delete keyword the way C and C++ do — you never manually release an object's memory.
This does not mean memory management disappears as a concern for a Java developer. You still need to understand roughly how and when memory gets reclaimed, because misunderstanding it leads to real problems: unnecessary GC pauses, and — despite having a garbage collector — genuine memory leaks.
Why it matters
Garbage collection removes a whole category of bugs, but it does not remove the need to think about memory. A service that slows down over several days and then fails with OutOfMemoryError nearly always has objects that are still reachable but no longer needed. To find and fix that, you need to know the one rule the collector follows: an object is kept for as long as it can be reached.
The Core Idea: Reachability, Not Reference Counting
An object becomes eligible for garbage collection when it is unreachable — meaning no active reference chain, starting from a "GC root" (such as a local variable currently on a thread's stack, a static field, or an active JNI reference), leads to it anymore. Simply setting a reference to null, letting a local variable go out of scope, or reassigning a reference are all common ways an object becomes unreachable. Note this is different from simple reference counting: two objects can hold references only to each other (an "island" of mutual references) and still both be correctly identified as garbage, because neither is reachable from a GC root.
The Mark-and-Sweep Idea (Conceptually)
At a conceptual level — without tying this to any one JVM's specific implementation details — most Java garbage collectors work in two logical phases. First, a "mark" phase starts from the set of GC roots and walks every reachable reference, marking each object it finds along the way as "live." Second, a "sweep" phase scans the heap and reclaims the memory of every object that was not marked, because anything unmarked is, by definition, unreachable garbage. Modern production JVMs use much more sophisticated collectors built on this same underlying idea (generational collection, concurrent marking, compaction, and so on), but the mark-then-reclaim logic is the mental model worth keeping.
System.gc() Is a Hint, Not a Command
Java exposes System.gc() (and the equivalent Runtime.getRuntime().gc()), but calling it does not force garbage collection to happen immediately, or at all. It is only a suggestion to the JVM that "now might be a good time to run garbage collection" — the JVM is completely free to ignore it. Relying on System.gc() for correctness (for example, expecting a finalizer to run right after calling it) is considered bad practice; real GC timing is controlled by the JVM's own heuristics.
Yes, Java Programs Can Still Leak Memory
A "memory leak" in Java doesn't mean memory that's technically lost the way it can be in C — it means objects that are still reachable, and therefore never eligible for collection, even though the program logically no longer needs them. Because the GC only reclaims unreachable objects, any accidental reference that keeps an unwanted object reachable prevents it from ever being freed.
- Static collections that keep growing — a
static Listorstatic Mapthat objects get added to but never removed from keeps every one of those objects reachable for the entire life of the application. - Listeners/callbacks that are registered but never unregistered — the object holding the listener reference (e.g. an event bus) keeps the listener, and everything it references, alive indefinitely.
- Long-lived caches with no eviction policy — a cache meant to be short-term that never expires or evicts entries effectively becomes a permanent, ever-growing GC root chain.
- Inner (non-static) classes holding an implicit reference to their enclosing instance, kept alive somewhere long-lived, unintentionally keeping the whole outer object reachable too.
Example
An object becoming unreachable and eligible for GC
public class GcDemo {
public static void main(String[] args) {
StringBuilder data = new StringBuilder("large payload");
System.out.println("Before: " + data);
data = null; // no reference chain from a GC root reaches the old StringBuilder anymore
System.out.println("Reference cleared; original object is now eligible for garbage collection");
System.gc(); // only a HINT to the JVM — collection is not guaranteed to run here
System.out.println("Requested GC (JVM may or may not act on it immediately)");
}
}
Before: large payload
Reference cleared; original object is now eligible for garbage collection
Requested GC (JVM may or may not act on it immediately)
Where this is used
Memory leaks in production are diagnosed from a heap dump, which is a snapshot of every object on the heap. Starting the JVM with -XX:+HeapDumpOnOutOfMemoryError writes one automatically when the heap runs out. Opening it in a tool such as Eclipse MAT or VisualVM shows which objects take the most memory and which chain of references keeps them reachable. That chain usually ends at a static collection or a cache with no size limit.
Common Mistakes
- Calling System.gc() and assuming garbage collection has now definitely happened — it is only a hint the JVM is free to ignore or delay.
- Assuming Java code can never have a memory leak because "Java has garbage collection" — reachable-but-unwanted objects (static collections, unregistered listeners, unbounded caches) leak just as real memory in Java as in any language.
- Confusing setting a reference to null with actually freeing memory — nulling a reference only makes the object eligible for collection; reclamation happens later, on the GC's own schedule.
- Writing code that depends on finalizers or GC timing for correctness (e.g., closing a file only in a finalizer) instead of using try-with-resources or explicit close() calls.
Exercise
The static list below grows for as long as the program runs, so everything added to it stays reachable. Change remember so the list keeps only the 3 most recent terms.
Expected output: 3 then [c, d, e]
import java.util.ArrayList;
import java.util.List;
public class RecentSearches {
private static final int LIMIT = 3;
private static final List<String> recent = new ArrayList<>();
static void remember(String term) {
recent.add(term);
// TODO: if the list is now longer than LIMIT, remove the oldest term
}
public static void main(String[] args) {
for (String term : new String[] {"a", "b", "c", "d", "e"}) {
remember(term);
}
System.out.println(recent.size());
System.out.println(recent);
}
}
Show hints
- The oldest term is at index 0.
recent.remove(0)removes it. Once the list no longer refers to that string, the string can be collected.
Show solution
import java.util.ArrayList;
import java.util.List;
public class RecentSearches {
private static final int LIMIT = 3;
private static final List<String> recent = new ArrayList<>();
static void remember(String term) {
recent.add(term);
if (recent.size() > LIMIT) {
recent.remove(0); // the oldest term is no longer reachable from the list
}
}
public static void main(String[] args) {
for (String term : new String[] {"a", "b", "c", "d", "e"}) {
remember(term);
}
System.out.println(recent.size()); // 3
System.out.println(recent); // [c, d, e]
}
}
Quick quiz
1. When does an object become eligible for garbage collection?
- A. When its variable goes out of scope
- B. When its reference count is zero
- C. When System.gc() is called
- D. When no chain of references from a GC root reaches it
Show answer
D. The test is reachability from GC roots such as local variables on a stack and static fields.
2. What does System.gc() do?
- A. Suggests that the JVM run a collection; the JVM may ignore it
- B. Runs a full collection immediately
- C. Frees all objects set to null
- D. Stops the program until memory is free
Show answer
A. It is a request. The JVM decides when to collect.
3. Objects A and B refer only to each other, and nothing else refers to either. What happens to them?
- A. They are never collected
- B. Both are eligible for collection
- C. Only A is collected
- D. The JVM throws an error
Show answer
B. Neither is reachable from a GC root, so both are garbage. Java does not rely on reference counting.
Interview questions
How does garbage collection work in Java?
Show answer
The collector starts from the GC roots — local variables on thread stacks, static fields and a few others — and follows every reference to mark the objects that are reachable. Everything not marked is garbage and its memory is reclaimed. Most collectors are also generational: new objects are allocated in a young area that is collected often, because most objects become unreachable soon after they are created, and objects that survive are moved to an old area that is collected less often.
Can a Java program have a memory leak?
Show answer
Yes. A leak in Java is an object that the program no longer needs but that is still reachable, so the collector must keep it. Common causes are a static collection that only grows, a cache with no eviction, and listeners that are registered and never removed. The fix is to remove the reference, for example by limiting the size of the cache or unregistering the listener.
Why should you not rely on finalize() to release resources?
Show answer
There is no guarantee of when finalize() runs, or that it runs at all, so a file or connection released there may stay open indefinitely. It has been deprecated since Java 9. Resources should be closed with try-with-resources or an explicit close() call.
Key Points to Remember
- Garbage collection automatically reclaims heap memory for objects that are no longer reachable from any GC root — this is why Java has no free()/delete.
- The conceptual model is mark (find everything reachable) then sweep (reclaim everything not marked); mutually-referencing but unreachable "islands" are still collected correctly.
- System.gc() only requests garbage collection — it never guarantees it will run immediately, or at all.
- Memory leaks are still possible in Java when objects are unintentionally kept reachable via static collections, unregistered listeners, or unbounded caches.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Runs plain Java 17. Frameworks such as Spring, and databases, are not available here. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering