3 minutes
4: Garbage Collection in Java
Java Memory Model and Garbage Collection – How Objects Live and Die
Introduction
In Java, you never explicitly free memory. Instead, the Java Virtual Machine (JVM) automatically allocates and reclaims memory for you. Understanding the Java memory model, how the stack, heap, and metaspace work together, and how garbage collection (GC) runs can help you write efficient, leak-free code.
1. Java Memory Regions: Stack, Heap, and Metaspace
- Stack: Each thread has its own stack for primitive local variables and method call frames. When a method is invoked, Java pushes a new frame; when it returns, that frame is popped. Stack memory is fast but limited in size.
- Heap: This is where all objects live. The heap is created at JVM startup and grows or shrinks within limits to accommodate object allocation. The garbage collector runs on the heap to reclaim space from objects that are no longer reachable.
- Metaspace: Since Java 8, class metadata is stored here instead of the old PermGen space. Metaspace grows by default until it hits system memory limits, reducing out-of-memory errors related to class loading.
Teaching Tip: Draw a diagram showing a thread’s stack and the shared heap and metaspace to help students visualize where data lives.
2. Generations in the Heap
To optimize GC pause times, the heap is typically divided into young and old generations:
- Young Generation (Nursery): Newly created objects go here. It’s subdivided into Eden and two Survivor spaces. Minor GCs occur frequently and are fast. Most objects die young.
- Old Generation: Objects that survive enough minor GCs get promoted here. Major GCs run less often but take longer.
Teaching Tip: Show how promotion thresholds work by allocating short-lived versus long-lived objects in a simple program.
3. How Garbage Collection Works
The basic GC process involves three phases:
- Marking: Identify which objects are still reachable from roots (static fields, local variables, etc.).
- Deletion: Remove unreachable objects and reclaim their space.
- Compaction: (Optional) Move surviving objects together to reduce fragmentation.
Since Java 9, the default collector in HotSpot is G1GC, which divides the heap into many regions and collects them in parallel to minimize pauses. Newer JVMs offer ZGC (Java 11+) and Shenandoah (Java 12+) for ultra-low-pause GC. (en.wikipedia.org)
Teaching Tip: Demonstrate how different GC algorithms affect pause times by toggling -XX:+UseG1GC
vs. -XX:+UseZGC
and measuring GC logs.
4. Finalization and Resource Management
Relying on finalize()
is discouraged; it’s unpredictable and often removed in future Java versions. Instead, use try-with-resources and implement AutoCloseable
to deterministically release non-memory resources (files, sockets, etc.).
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
// use reader
}
Teaching Tip: Compare finalize()
vs. try-with-resources in a live demo to show why explicit cleanup is safer.
5. Common Memory Pitfalls
- Memory leaks can still happen if you hold references unnecessarily (e.g., static lists, caches without eviction).
- Large object retention: Keeping big objects alive inadvertently can starve the young generation.
- OutOfMemoryError: Happens when the heap is exhausted and GC cannot reclaim enough space.
Best Practices:
- Use weak references (
WeakHashMap
) for caches. - Monitor heap usage with tools like VisualVM or JConsole.
- Tune heap sizes (
-Xms
,-Xmx
) and GC settings based on your application’s profile.
Key Takeaways
- Java’s memory is divided into stack, heap, and metaspace regions.
- Generational GC (young vs. old) optimizes for short-lived objects.
- Modern collectors like G1GC, ZGC, and Shenandoah minimize pause times.
- Prefer try-with-resources over finalizers for resource cleanup.
- Watch out for memory leaks by releasing unneeded references.
In the next post, we’ll dive into concurrency, threads, synchronization, and the Java Memory Model’s happens-before guarantees. Stay tuned!