How does garbage collection work internally?
Java's automatic memory management through Garbage Collection (GC) is a cornerstone feature that frees developers from manual memory deallocation, preventing common issues like memory leaks and dangling pointers. Internally, GC is a complex, multi-stage process designed to efficiently reclaim memory occupied by objects that are no longer reachable by the application.
The Core Concept: Reachability
At its heart, Java GC operates on the principle of reachability. An object is considered 'live' and kept in memory if it can be reached from a set of 'GC roots.' Objects that are no longer reachable by any active part of the application are deemed 'garbage' and eligible for collection.
GC Roots
GC roots are special objects that are always considered reachable and serve as the starting points for the GC algorithm to trace object references. Examples include:
- Local variables and parameters in the currently executing methods.
- Active threads.
- Static fields of loaded classes.
- JNI (Java Native Interface) references.
Generational Hypothesis and Heap Structure
Most modern JVM GCs are generational, based on the 'generational hypothesis' which states that most objects are short-lived, and a few objects are very long-lived. This hypothesis leads to dividing the heap into different generations:
- Young Generation: Where new objects are initially allocated. It's further divided into Eden space and two Survivor spaces (S0 and S1). Most objects are created here and die quickly.
- Old (Tenured) Generation: Objects that survive multiple garbage collection cycles in the Young Generation are promoted here. These objects are expected to be long-lived.
- Metaspace (Java 8+) / Permanent Generation (Java 7 and earlier): Stores metadata about classes, methods, and other internal JVM data. This is typically not part of the Java heap and is collected separately, usually when classloaders are deallocated.
The Basic Algorithm: Mark-and-Sweep
The fundamental algorithm employed by many GCs is 'Mark-and-Sweep,' often extended with 'Compact'.
1. Mark Phase
Starting from the GC roots, the garbage collector traverses the object graph, identifying and marking all reachable (live) objects. Any object not marked after this phase is considered garbage.
2. Sweep Phase
The collector iterates through the heap, reclaiming memory from the unmarked objects. This involves adding the memory regions of these objects back to a list of free memory.
3. Compact Phase (Optional but common)
After sweeping, memory can become fragmented (small, non-contiguous blocks of free space). Compaction moves live objects together to eliminate fragmentation, making it easier to allocate larger objects subsequently. This often involves copying objects.
Stop-The-World (STW) Pauses
For the GC to accurately identify reachable objects, the application's threads must often be paused entirely. This is known as a 'Stop-The-World' (STW) pause. During STW, no application code executes, which can impact responsiveness. Modern GCs aim to minimize the duration and frequency of these pauses, often by performing much of the work concurrently with the application.
Minor, Major, and Full GC
- Minor GC: Occurs when the Young Generation runs out of space. It's typically fast and involves collecting garbage only within the Young Generation. Live objects are moved between Survivor spaces or promoted to the Old Generation.
- Major GC: Refers to garbage collection in the Old Generation. Often, a Major GC implies a Full GC, but some collectors (like CMS) can perform Old Generation collection independently.
- Full GC: Involves collecting the entire heap (both Young and Old Generations). Full GCs are generally more expensive and result in longer STW pauses.
Different Garbage Collectors
The JVM offers several GC implementations, each with different performance characteristics and goals:
- Serial GC: Single-threaded, simple, suitable for client-side applications with small heaps. All GC work is done by one thread during a STW pause.
- Parallel GC (Throughput Collector): Uses multiple threads for minor and major collections. Designed for applications that prioritize throughput (total work done) over latency (pause times).
- CMS (Concurrent Mark-Sweep) GC: Aims for lower latency by performing most of its work concurrently with application threads. It has short STW pauses but can lead to heap fragmentation and requires more CPU resources.
- G1 (Garbage-First) GC: A region-based collector designed for large heaps, aiming for more predictable and configurable pause times. It divides the heap into regions and processes regions with the most garbage first.
- ZGC / Shenandoah (Low-Latency Collectors): Designed for extremely low-latency requirements, even with very large heaps (terabytes). They perform almost all their work concurrently with minimal STW pauses, utilizing advanced techniques like colored pointers and read barriers.
Internal Optimization Techniques
To improve efficiency, GCs use various optimizations:
- Card Tables / Remembered Sets: Used by generational collectors to track references from the Old Generation to the Young Generation, avoiding scanning the entire Old Generation during a Minor GC.
- Write Barriers: Code inserted by the JVM when an object's reference field is modified, used to maintain data structures like card tables or remembered sets.
- Read Barriers: Code inserted by the JVM when an object's reference field is read, typically used by ultra-low-latency collectors (like ZGC, Shenandoah) to ensure consistency during concurrent compaction.