Garbage Collection in Java – How Automatic Memory Management Works
Garbage collection (GC) is the JVM's automatic memory management. Your program creates objects with new and never frees them explicitly; instead, the garbage collector periodically finds objects that can no longer be reached and reclaims their memory. This removes whole classes of bugs common in C and C++, such as double frees and dangling pointers.
This tutorial explains when an object becomes eligible for collection, how the heap is organised into generations, the collectors available in modern Java (G1, ZGC, Parallel and Serial), what System.gc() really does, and how memory leaks can still happen.
1. When Is an Object Garbage?
An object is eligible for garbage collection when it is no longer reachable from any GC root. GC roots include local variables and parameters of running methods, static fields of loaded classes, active threads and JNI references. The collector traces every reference from the roots; anything it cannot reach is garbage.
The program below uses WeakReference (which does not keep an object alive) to observe whether objects are actually collected:
import java.lang.ref.WeakReference;
public class Eligibility {
static class Report {
final String name;
Report(String name) { this.name = name; }
}
// Returns true once the object behind 'ref' has actually been collected
static boolean collected(WeakReference<?> ref) throws InterruptedException {
for (int i = 0; i < 20 && ref.get() != null; i++) { System.gc(); Thread.sleep(20); }
return ref.get() == null;
}
public static void main(String[] args) throws InterruptedException {
// 1. Nulling the only reference
Report r1 = new Report("daily");
WeakReference<Report> w1 = new WeakReference<>(r1);
r1 = null;
System.out.println("1. reference set to null -> collected: " + collected(w1));
// 2. Reassigning the reference
Report r2 = new Report("weekly");
WeakReference<Report> w2 = new WeakReference<>(r2);
r2 = new Report("monthly");
System.out.println("2. reference reassigned -> collected: " + collected(w2));
// 3. Object created inside a method that has returned
WeakReference<Report> w3 = createAndForget();
System.out.println("3. method-local object -> collected: " + collected(w3));
// 4. Island of isolation: two objects refer only to each other
Object[] a = new Object[1], b = new Object[1];
a[0] = b; b[0] = a;
WeakReference<Object> w4 = new WeakReference<>(a);
a = null; b = null;
System.out.println("4. island of isolation -> collected: " + collected(w4));
// Still referenced: never collected
Report kept = new Report("audit");
WeakReference<Report> w5 = new WeakReference<>(kept);
System.out.println(" still referenced -> collected: " + collected(w5) + " (" + kept.name + ")");
}
static WeakReference<Report> createAndForget() {
Report temp = new Report("temp");
return new WeakReference<>(temp);
}
}
1. reference set to null -> collected: true 2. reference reassigned -> collected: true 3. method-local object -> collected: true 4. island of isolation -> collected: true still referenced -> collected: false (audit)
Common ways an object becomes unreachable:
- The only reference is set to
null. - The reference is reassigned to another object.
- The method that created it returns, and nothing else refers to it.
- Objects refer only to each other (an island of isolation). Java's tracing collectors handle cycles, unlike simple reference counting.
2. How the Heap Is Organised: Generations
Most objects die young: temporary strings, iterators and request objects live for microseconds. Collectors exploit this weak generational hypothesis by dividing the heap:
| Area | Contents | Collected by |
|---|---|---|
| Young generation: Eden + two Survivor spaces | Newly created objects | Frequent, fast minor (young) collections |
| Old (tenured) generation | Objects that survived several young collections | Less frequent major / mixed collections |
| Metaspace (native memory, not heap) | Class metadata | Freed when class loaders are unloaded |
New objects are allocated in Eden. Live objects are copied to a Survivor space at each minor GC; after surviving enough cycles they are promoted to the old generation.
3. Mark, Sweep and Compact
- Mark: trace from the GC roots and mark every reachable object.
- Sweep: reclaim the memory of unmarked objects.
- Compact / copy: move live objects together to avoid fragmentation, so allocation stays a fast pointer bump.
Some work requires stop-the-world pauses, when application threads are halted. Modern collectors do most work concurrently with the application to keep pauses short.
4. Garbage Collectors in Modern Java
| Collector | Flag | Strength |
|---|---|---|
| G1 (default since Java 9) | -XX:+UseG1GC | Balanced throughput and pause times; region-based heap |
| ZGC (generational since Java 21) | -XX:+UseZGC | Sub-millisecond pauses, even with terabyte heaps |
| Shenandoah | -XX:+UseShenandoahGC | Low pauses with concurrent compaction |
| Parallel | -XX:+UseParallelGC | Maximum throughput for batch jobs; longer pauses |
| Serial | -XX:+UseSerialGC | Single thread; small heaps and containers with one CPU |
The old CMS collector was removed in Java 14. Useful options: -Xms/-Xmx (initial and maximum heap) and -Xlog:gc to log every collection.
5. System.gc() and Memory Statistics
System.gc() (or Runtime.getRuntime().gc()) only suggests a collection. The JVM may ignore it (for example with -XX:+DisableExplicitGC). Runtime also reports memory usage:
public class MemoryStats {
public static void main(String[] args) {
Runtime rt = Runtime.getRuntime();
long mb = 1024 * 1024;
long before = rt.totalMemory() - rt.freeMemory();
byte[][] garbage = new byte[50][];
for (int i = 0; i < garbage.length; i++) garbage[i] = new byte[1024 * 1024]; // ~50 MB
long during = rt.totalMemory() - rt.freeMemory();
garbage = null; // all 50 arrays become unreachable
System.gc(); // a request, not a command
long after = rt.totalMemory() - rt.freeMemory();
System.out.println("Used memory grew by ~50 MB when allocating: " + (during - before >= 45 * mb));
System.out.println("Used memory dropped after GC: " + (after < during - 40 * mb));
System.out.println("Max heap (-Xmx) is available via Runtime.maxMemory(): " + (rt.maxMemory() > 0));
}
}
Used memory grew by ~50 MB when allocating: true Used memory dropped after GC: true Max heap (-Xmx) is available via Runtime.maxMemory(): true
Do not call System.gc() in production code; the collector knows better when to run, and forced full collections cause unnecessary pauses.
6. finalize() Is Deprecated: Use Cleaner and try-with-resources
Object.finalize() was called by the GC before reclaiming an object, but it ran unpredictably or not at all and slowed collection. It is deprecated for removal. Release resources with AutoCloseable and try-with-resources; use java.lang.ref.Cleaner only as a safety net:
import java.lang.ref.Cleaner;
public class CleanerDemo {
private static final Cleaner CLEANER = Cleaner.create();
// A resource that should be closed explicitly; Cleaner is only a safety net
static class TempFile implements AutoCloseable {
private final Cleaner.Cleanable cleanable;
TempFile(String path) {
// The cleanup action must NOT reference 'this'
this.cleanable = CLEANER.register(this, () -> System.out.println("deleted " + path));
}
@Override public void close() { cleanable.clean(); } // runs the action at most once
}
public static void main(String[] args) {
try (TempFile f = new TempFile("/tmp/report.csv")) {
System.out.println("working with the file");
}
System.out.println("after try-with-resources");
}
}
working with the file deleted /tmp/report.csv after try-with-resources
See final vs finally vs finalize.
7. Memory Leaks in a Garbage-Collected Language
GC frees unreachable objects, but objects that are still reachable and no longer needed stay in memory forever. Typical causes:
- Ever-growing static collections or caches without eviction (use bounded caches).
- Listeners and callbacks that are registered but never removed.
- Inner or anonymous class instances holding references to large outer objects.
ThreadLocalvalues not removed in thread pools.- Unclosed resources such as streams and connections.
Symptoms are growing heap usage and eventually OutOfMemoryError: Java heap space. Diagnose with heap dumps (-XX:+HeapDumpOnOutOfMemoryError), jcmd, VisualVM, JDK Mission Control or Eclipse MAT.
8. Reference Types
| Type | Collected when | Use |
|---|---|---|
| Strong (normal) | Never while reachable | Everyday references |
SoftReference | Memory is running low | Memory-sensitive caches |
WeakReference | Next GC, if only weakly reachable | WeakHashMap, canonicalising maps |
PhantomReference | After the object is finalised; get() always returns null | Post-mortem clean-up (Cleaner uses it) |
9. Summary
- The JVM automatically reclaims objects that are unreachable from GC roots, including cycles.
- The heap is split into young and old generations, because most objects die young.
- G1 is the default collector; ZGC and Shenandoah offer very short pauses; Parallel maximises throughput.
System.gc()is only a hint;finalize()is deprecated in favour of try-with-resources andCleaner.- Leaks still happen when unneeded objects stay reachable.
Related Tutorials
Frequently Asked Questions (FAQ)
Q1. What is garbage collection in Java?
Automatic memory management in which the JVM finds objects that are no longer reachable and reclaims their memory.
Q2. When is an object eligible for garbage collection?
When no chain of strong references leads to it from any GC root, such as local variables of active methods or static fields.
Q3. Can we force garbage collection?
No. System.gc() only requests it, and the JVM may ignore the request.
Q4. Which garbage collector is the default?
G1 has been the default since Java 9 on server-class machines. The JVM may pick Serial on very small machines or containers.
Q5. What are GC roots?
References the collector starts tracing from: local variables and parameters on thread stacks, static fields, active threads, JNI references and some JVM-internal references.
Q6. Does Java garbage collection handle circular references?
Yes. Tracing collectors only care about reachability from roots, so unreachable cycles are collected.
Q7. What is the difference between minor and major GC?
A minor GC collects the young generation and is usually fast. A major or full GC collects the old generation (or the whole heap) and takes longer.
Q8. What is a stop-the-world pause?
A period during which the JVM halts all application threads so the collector can safely perform certain work.
Q9. What is ZGC?
A scalable, low-latency collector that does almost all its work concurrently, keeping pauses below a millisecond regardless of heap size. It became generational in Java 21.
Q10. Is finalize() still used?
It is deprecated for removal. Use try-with-resources for deterministic clean-up and Cleaner as a fallback.
Q11. Can Java have memory leaks?
Yes, when objects that are no longer needed remain reachable, for example in static collections, caches or registered listeners.
Q12. What causes OutOfMemoryError?
The JVM cannot allocate an object because the heap (or another area such as Metaspace) is full even after garbage collection, often due to a leak or a too-small -Xmx.
Q13. What is Metaspace?
Native memory where the JVM stores class metadata. It replaced PermGen in Java 8.
Q14. Does setting a reference to null help GC?
Rarely needed for local variables, which go out of scope. It helps for long-lived fields or array slots that would otherwise keep large objects reachable.
Q15. What are -Xms and -Xmx?
JVM options for the initial and maximum heap size, for example -Xms512m -Xmx2g.
Q16. How can I see what the garbage collector is doing?
Enable logging with -Xlog:gc*, or use jstat, jcmd, VisualVM or JDK Mission Control.
Q17. What is the difference between WeakReference and SoftReference?
A weakly reachable object is collected at the next GC. A softly reachable object is kept until the JVM is short of memory.
Q18. Are static variables garbage collected?
Objects referenced by static fields stay alive as long as the class is loaded, which is usually the whole application lifetime.
Q19. Which collector should I choose?
Start with the default G1. Choose ZGC for latency-sensitive services with large heaps and Parallel for throughput-oriented batch jobs, then measure.
Q20. Is garbage collection the same as freeing memory in C?
No. In C you call free() explicitly. In Java the collector decides when unreachable memory is reclaimed, so programs cannot double-free or use freed memory.