All topics
memory management
intermediate

Garbage Collection

Free heap memory automatically by identifying objects no longer reachable from GC roots — and choose the GC algorithm that fits your latency vs throughput target.

Garbage collection frees Java programmers from manual memory management by automatically reclaiming objects that can no longer be reached. The core idea is reachability: starting from a set of GC roots — local variables on thread stacks, static fields, JNI references — the collector traces every object reachable by following references. Anything not reached is unreachable and its memory can be reclaimed. Crucially, this is about reachability, not reference counting, which is why a self-referential island of objects with no path from a root is still collected.

A janitor that throws out anything no longer connected to the main building (GC roots). G1 cleans one room at a time during business hours. ZGC works alongside everyone without disturbing them.

Key Concepts

1
Most modern collectors are generational, built on the empirical observation that the vast majority of objects die young. New objects are allocated in the young generation (Eden); a minor GC quickly sweeps it, copying the few survivors between survivor spaces and eventually promoting long-lived ones to the old generation. The old generation is collected less often by a more expensive major/full GC. The algorithms trade latency against throughput: the throughput-oriented Parallel GC maximises total work done but pauses longer; G1 (the default since Java 9) divides the heap into regions and targets predictable pause goals; and ZGC and Shenandoah do most of their work concurrently to keep pauses in the low-millisecond range even on huge heaps.
2
For interviews and tuning, a few points matter. GC reclaims memory but cannot prevent leaks: an object still referenced from a root — a growing static collection, an unremoved listener — will never be collected no matter how "unused" it is logically. finalize() is deprecated and unreliable; use try-with-resources for cleanup. Calling System.gc() is only a suggestion and generally a code smell. And tuning is mostly about choosing the right collector for your goal — low pause time versus maximum throughput — and sizing the heap and generations to match the application's allocation pattern.
finalize()try-with-resourcesSystem.gc()