Java Garbage Collection: How JVM Manages Memory
Java developers rarely think about freeing memory. They create objects, use them, and move on. The JVM, through its garbage collector, silently reclaims memory from objects that are no longer needed. This automation is one of Java’s greatest strengths—it eliminates entire categories of bugs that plague manual memory management—but it also introduces a new set of challenges: pause times, allocation pressure, and logical memory leaks.
Understanding how garbage collection works is not an academic luxury. It is essential for diagnosing production memory problems, tuning performance, and designing applications that behave well under load. This article establishes the conceptual foundation: object lifecycle, reachability, the generational hypothesis, the major collectors, and how GC affects application performance. The deeper tuning and troubleshooting techniques belong to the Performance Engineering section, but the model you build here will make those advanced topics intuitive.
1. Why Garbage Collection Exists​
In languages with manual memory management (C, C++), the programmer is responsible for allocating and freeing memory. This introduces several failure modes:
- Memory leaks – allocated memory is never freed, causing the application’s footprint to grow until the system runs out of memory.
- Dangling pointers – memory is freed, but a pointer still references it. Reading or writing through that pointer corrupts memory.
- Double frees – freeing the same memory twice can corrupt the allocator’s internal structures.
- Use‑after‑free – accessing memory after it has been freed leads to undefined behavior.
Garbage collection removes the programmer from this loop. The JVM automatically identifies objects that are no longer reachable and reclaims their memory. The developer’s model changes from “allocate → use → free” to “allocate → use → forget.”
The trade‑off is that GC consumes CPU and can introduce pauses. You trade a class of correctness bugs for a class of performance challenges. A professional Java engineer understands both sides of that bargain.
2. Java Object Lifecycle​
Every object on the heap follows a lifecycle:
Allocation
↓
Reachable
↓
Used by Application
↓
No Longer Reachable
↓
Eligible for GC
↓
Reclaimed
- Allocation – the
newoperator allocates memory from the heap. - Reachable – the object is referenced by at least one live path from a GC root.
- No longer reachable – all references to the object have been removed or gone out of scope.
- Eligible for GC – the object may be collected, but collection is not immediate.
- Reclaimed – the garbage collector frees the memory occupied by the object.
Becoming unreachable does not mean the object is immediately freed. It only means it is a candidate for collection. The actual reclamation happens at the collector’s discretion.
3. Heap Memory and Object Allocation​
The JVM heap is the runtime data area where all class instances and arrays are allocated. It is shared among all threads, which means allocation must be thread‑safe and fast. HotSpot achieves this with Thread‑Local Allocation Buffers (TLABs)—small per‑thread regions within Eden where objects are allocated with a simple pointer bump, no synchronization required.
Traditionally, the heap is divided into generations:
Java Heap
├── Young Generation
│ ├── Eden (new objects)
│ ├── Survivor Space 0 (from)
│ └── Survivor Space 1 (to)
└── Old Generation (promoted objects)
This division is based on the weak generational hypothesis: most objects die young. By focusing collection effort on the young generation, the JVM reclaims a lot of memory quickly without scanning the entire heap. Older objects, which have survived several collections, are promoted to the old generation, which is collected less frequently.
It is important to note that the specific heap layout varies by collector. The generational model is a HotSpot implementation strategy, not a JVM specification requirement. Modern collectors like ZGC may use different structures, but the conceptual idea of separating short‑lived and long‑lived objects remains useful.
4. GC Roots and Reachability​
Garbage collection starts from a set of GC roots—objects that are always considered live. From these roots, the collector traverses references to identify all reachable objects. Anything not reached is garbage.
Common GC roots include:
- Active thread stacks – local variables and method parameters of running threads.
- Static fields – static variables of loaded classes.
- JNI references – objects held by native code through JNI.
- JVM internal structures – e.g., class metadata, interned strings.
GC Roots
│
├── Object A
│ └── Object B
├── Object C
│ └── Object D
└── Object E
Unreachable Objects
├── Object X
└── Object Y
Objects A, B, C, D, and E are reachable and will survive collection. Objects X and Y have no path from any root and are candidates for reclamation.
Reachability is a graph problem. If an object is reachable, it is live. If it is not reachable, it is garbage. This distinction is fundamental.
5. Reference Types in Java​
Java defines four reference types, in descending order of strength:
| Reference Type | Description | GC Interaction |
|---|---|---|
| Strong | Normal object references (Object o = new Object();) | Object is never collected while strongly reachable. |
| Soft | SoftReference<T> | Collected only when the JVM is low on memory; useful for caches. |
| Weak | WeakReference<T> | Collected during the next GC cycle if only weakly reachable. |
| Phantom | PhantomReference<T> | Allows post‑mortem cleanup; object is already finalized (or not). |
Soft and weak references allow the application to hold onto objects without preventing their collection. They are used in caches and canonical mappings, but should be applied with care: a soft reference cache can still cause high memory pressure if not bounded.
6. How Garbage Collection Works​
All GC algorithms perform a variation of three fundamental operations:
- Mark – traverse the object graph from roots and mark every reachable object as live.
- Sweep – identify the memory occupied by unmarked objects and make it available for future allocation.
- Compact – move live objects together to eliminate fragmentation and improve allocation locality.
Application Objects
↓
Mark
↓
Identify Live Objects
↓
Sweep / Copy / Evacuate
↓
Reclaim Memory
↓
Compact When Required
Different collectors combine these operations in different ways:
- Mark‑sweep – mark live objects, then sweep the heap to free dead objects. Can cause fragmentation.
- Mark‑compact – mark live objects, then slide them to one end of the heap, reclaiming a contiguous free region.
- Copying – split the heap into two semispaces; live objects are copied from one to the other, and the source semispace becomes free.
- Evacuation – similar to copying but used in region‑based collectors like G1; live objects from a region are copied to another region, and the source region is reclaimed entirely.
Modern collectors often run the mark phase concurrently with the application, but the actual reclamation (sweep, compact, evacuate) may still require a pause.
7. Stop‑the‑World and Concurrent Collection​
A stop‑the‑world (STW) pause is a period during which all application threads are halted so that the collector can perform work safely. Historically, all GC work happened during STW pauses. Modern collectors have pushed most work into concurrent phases that run while the application continues.
Key terminology:
- Mutator – the application threads that allocate and modify objects.
- Stop‑the‑world – the collector suspends all mutators.
- Concurrent – collector work runs while mutators are active.
- Incremental – collection work is broken into small chunks interleaved with mutator execution.
It is crucial to understand that concurrent does not mean pause‑free. Even ZGC and Shenandoah, the most concurrent collectors, still have short STW pauses (often sub‑millisecond) for specific operations like root scanning. The goal is to make pauses so short and predictable that they do not affect the application’s latency requirements.
8. Minor, Major, and Full GC​
These terms are commonly used but not precisely standardized across all collectors.
- Minor GC / Young GC – collection of the young generation only. Usually fast because the young generation is small and most objects are garbage.
- Major GC / Old GC – collection of the old generation. Often more expensive because it scans a larger area with more live objects.
- Full GC – a collection of the entire heap (both young and old), often including metadata and other areas. Full GC typically causes the longest pauses.
Because terminology varies, it is better to describe collections by which area they target and whether they are STW or concurrent. In G1, for example, a “mixed GC” collects the young generation plus some old regions.
9. Generational Garbage Collection​
The generational approach exploits the weak generational hypothesis. New objects are allocated in Eden. When Eden fills, a minor GC is triggered:
- Live objects in Eden are copied to a survivor space.
- Objects that have survived several minor collections are promoted to the old generation.
- Unreachable objects are simply discarded.
The flow looks like:
Eden (new objects)
↓ minor GC
Survivor Space 0
↓ next minor GC
Survivor Space 1
↓ after aging threshold
Old Generation
The aging threshold (tenuring threshold) determines how many minor GC cycles an object must survive before being promoted. The JVM dynamically adjusts this threshold based on survivor space utilization.
Generational collection works well for typical workloads: most objects die in Eden, so minor GC is frequent but cheap. The old generation, containing long‑lived objects, is collected less often but with potentially longer pauses.
10. Major Java Garbage Collectors​
HotSpot offers several collectors, each designed for different trade‑offs.
Serial GC​
- Single‑threaded collection for both young and old generations.
- Simplest implementation, smallest memory footprint.
- Suitable for small applications, embedded environments, and development machines.
- Enabled with
-XX:+UseSerialGC.
Parallel GC​
- Multi‑threaded young and old generation collection.
- Prioritizes throughput (total work done) over pause time.
- Default on many server JVMs before G1 became default in JDK 9.
- Enabled with
-XX:+UseParallelGC.
G1 GC​
- Region‑based heap, generational design.
- Splits heap into small regions (typically 1–32 MB); young and old are logical sets of regions.
- Uses concurrent marking and incremental evacuation to meet pause time goals.
- Default collector since JDK 9; a solid general‑purpose choice for most server applications.
- Enabled with
-XX:+UseG1GC.
ZGC​
- Designed for very low latency on large heaps (hundreds of GB to multi‑TB).
- Uses colored pointers and load barriers to allow concurrent compaction and object relocation.
- Pause times target sub‑millisecond, independent of heap size.
- Enabled with
-XX:+UseZGC(production since JDK 15).
Shenandoah GC​
- Also targets low latency with concurrent compaction.
- Uses Brooks pointers and forwarding pointers to allow concurrent object movement.
- Pause times also target sub‑millisecond.
- Available in OpenJDK builds that include it; enabled with
-XX:+UseShenandoahGC.
Each collector has strengths and weaknesses. The right choice depends on your workload, heap size, latency requirements, and available CPU.
11. Comparing Garbage Collectors​
| Collector | Primary Goal | Typical Strength | Trade‑off |
|---|---|---|---|
| Serial GC | Simplicity | Low memory footprint, predictable single‑thread behavior | Poor scalability on multi‑core |
| Parallel GC | Throughput | High total throughput for CPU‑bound batch jobs | Longer pauses, not suitable for latency‑sensitive apps |
| G1 GC | Balanced performance | Predictable pause times, good general‑purpose behavior | Slightly lower throughput than Parallel GC |
| ZGC | Ultra‑low latency | Sub‑millisecond pauses on huge heaps | Higher CPU overhead, more complex |
| Shenandoah | Ultra‑low latency | Concurrent compaction, low pauses | Higher CPU overhead, less mature in some environments |
Do not treat this as a simple ranking. The best collector is the one that meets your specific service‑level objectives (SLOs). Test with realistic workloads.
12. Garbage Collection and Application Performance​
GC affects performance in four primary ways:
- Latency – STW pauses delay application responses. If a request arrives during a pause, its processing is delayed.
- Throughput – time spent in GC is time not spent executing application code. Throughput collectors minimize total GC time, while latency collectors may spend more total time but in smaller chunks.
- CPU utilization – concurrent collectors use CPU resources that could otherwise serve application work.
- Memory footprint – the heap size and collector metadata consume memory. Larger heaps reduce GC frequency but may increase pause times.
Key metrics to monitor:
- Allocation rate – bytes allocated per second. High rates trigger frequent GC.
- GC frequency – how often collections occur.
- Pause duration – how long each STW pause lasts.
- Heap occupancy after GC – how much live data remains; a rising trend suggests a leak.
- Promotion rate – how fast objects move to the old generation.
The fundamental trade‑off is throughput vs. latency. A collector that minimizes total GC time (Parallel GC) may cause long pauses, while a collector that minimizes pause time (ZGC) may use more CPU.
13. Object Allocation and GC Pressure​
Not all object creation is equal. Temporary objects that die quickly are cheap for generational collectors; they are reclaimed in minor GCs without touching the old generation. However, excessive allocation—especially of large or long‑lived objects—puts pressure on the collector.
Common sources of allocation pressure:
- Autoboxing – converting primitives to wrapper objects in loops.
- String concatenation –
Stringis immutable; each concatenation creates a new object. UseStringBuilderfor loops. - Temporary collections – creating lists or maps inside hot methods.
- Large objects – objects larger than a threshold (typically half the region size in G1) are allocated directly in the old generation, causing earlier old‑gen collections.
Before optimizing allocation, measure. Modern JVMs handle high allocation rates well, and micro‑optimizing allocation can make code harder to read for negligible gain.
14. Memory Leaks in Garbage‑Collected Java​
A common misconception is that garbage collection eliminates memory leaks. It eliminates dangling pointers and double frees, but it does not prevent logical leaks—objects that are still reachable but no longer needed.
A leak occurs when references to objects are retained unintentionally. Examples:
- Static collections that grow without bound (e.g., a cache that never evicts entries).
ThreadLocalvariables not removed after use, especially in thread‑pooled environments.- Listeners or observers registered but never unregistered.
- Long‑lived objects holding references to short‑lived ones (e.g., a singleton holding a list of per‑request data).
Reachable ≠Useful
An object can be reachable from a GC root (so the GC cannot collect it) and yet be useless to the application. The heap will grow until OutOfMemoryError occurs. These leaks are diagnosed with heap dump analysis, not by looking at GC logs alone.
15. Garbage Collection and OutOfMemoryError​
OutOfMemoryError (OOM) is thrown when the JVM cannot allocate an object because the heap is full and no more memory can be reclaimed. It does not mean the GC failed; it means the live set of objects exceeds the available heap.
Common OOM scenarios:
- Java heap space – the live objects plus overhead fill the heap. Increasing heap or reducing live set may help.
- GC overhead limit exceeded – the JVM spends too much time in GC (more than 98%) and reclaims too little memory (less than 2% each cycle). Usually indicates a severe leak.
- Metaspace – class metadata exceeds the metaspace limit. Often caused by excessive class generation (e.g., dynamic proxies).
- Direct buffer memory – off‑heap memory allocated via
ByteBuffer.allocateDirect()is exhausted.
An OOM is a symptom, not a diagnosis. The root cause may be a leak, a heap that is too small, or an allocation pattern that creates too many live objects.
16. Monitoring Garbage Collection​
Modern JVMs provide rich observability:
- GC logs – unified logging (
-Xlog:gc*) records every collection with detailed timing, heap sizes, and promotion statistics. - Java Flight Recorder (JFR) – low‑overhead event recorder that includes GC events, allocation profiling, and heap statistics.
- Java Mission Control (JMC) – visualizes JFR recordings, showing GC pause times over time.
jstat– command‑line tool for live GC statistics.jcmd– can trigger GC, print heap histograms, and more.
When monitoring, focus on:
- Pause times – are they within your SLO?
- Heap occupancy trend – does live data grow continuously? (leak)
- GC frequency – is the collector running too often?
- Promotion rate – are too many objects being promoted to the old generation?
17. Garbage Collection Logging​
Modern JDKs use unified JVM logging. To enable detailed GC logging:
java -Xlog:gc* MyApplication
This prints GC events with timestamps, durations, and memory usage. The logs are essential for post‑mortem analysis and tuning.
Do not rely on logs alone; they tell you what happened but not why. Combine with heap dumps and profilers for a complete picture.
18. Common Garbage Collection Misconceptions​
- “GC immediately frees every unreachable object.” Not true. Collection is periodic and may be deferred.
- “More GC means a memory leak.” High GC frequency can also be caused by a very high allocation rate or a small heap. A leak is indicated by rising live set after collection.
- “GC eliminates all memory leaks.” Logical leaks remain; reachable but useless objects still consume heap.
- “Full GC always means the application is broken.” Occasional full GC may be normal, especially during startup or after a major change. Frequent full GC is a red flag.
- “Increasing the heap always fixes memory problems.” A larger heap may delay OOM but also increase pause times. It does not fix a leak.
- “ZGC and Shenandoah have zero pauses.” They have sub‑millisecond pauses, but not zero. Concurrent phases still have brief STW moments.
- “
System.gc()should be used to solve memory problems.”System.gc()is a hint, not a command. It often hurts performance and should be avoided except in very specific cases. - “GC performance can be understood from a single metric.” GC behavior is multi‑dimensional: allocation rate, live set, pause duration, frequency, and CPU overhead all interact.
19. Practical Example: Understanding Object Retention​
Consider this simple class:
public class Cache {
private static final Map<String, byte[]> DATA = new HashMap<>();
public static void add(String key, byte[] value) {
DATA.put(key, value);
}
}
Every call to add stores a byte array in a static HashMap. The map is a GC root (static field). All entries are reachable from that root. The GC cannot collect them, even if the application no longer uses them. If add is called repeatedly with new keys, the heap grows until OOM.
This is a logical leak. The objects are reachable, so GC works correctly—the problem is that the application is retaining them forever. Fixing it requires an eviction policy or removing entries when they are no longer needed.
20. Choosing a GC Strategy​
There is no one‑size‑fits‑all collector. Consider:
- Latency requirements – if p99 latency must stay below, say, 10 ms, you likely need ZGC or Shenandoah. If occasional 100 ms pauses are acceptable, G1 is fine.
- Throughput requirements – if the application is a batch processor with no interactive users, Parallel GC may give the highest total throughput.
- Heap size – ZGC and Shenandoah excel on multi‑hundred GB heaps; G1 works well up to tens of GB.
- CPU resources – concurrent collectors consume more CPU; ensure your pod has enough cores.
- Allocation rate – high allocation rates may favor collectors with low‑overhead young collections (Parallel GC, G1).
- Java version – newer versions contain performance improvements for all collectors.
The only reliable way to choose is to benchmark with production‑like workloads and measure the metrics that matter to you.
21. Garbage Collection and Performance Engineering​
GC tuning is one part of a broader performance engineering process:
Java Runtime
↓
Memory Allocation
↓
Garbage Collection
↓
GC Metrics
↓
Performance Analysis
↓
Optimization
The methodology:
- Measure – collect GC logs, JFR recordings, and application metrics.
- Establish a baseline – know your current pause times, allocation rate, and heap occupancy.
- Identify the bottleneck – is it GC pauses, high allocation, or a leak?
- Change one variable – adjust heap size, collector, or code allocation.
- Validate – rerun the workload and confirm the improvement.
GC tuning without measurement is guesswork. The Performance Engineering section covers these steps in detail.
22. Garbage Collection Learning Roadmap​
Object Lifecycle
↓
GC Roots
↓
Reachability
↓
Mark / Sweep / Compact
↓
Generational GC
↓
GC Collectors
↓
GC Metrics
↓
GC Tuning
↓
Production Troubleshooting
Follow this order: understand the lifecycle, then reachability, then collection algorithms. Generational concepts come next, then the specific collectors. Only after that do you study metrics and tuning. Production troubleshooting is the final application of everything.
23. Frequently Asked Questions​
What is Garbage Collection in Java? GC is the automatic reclamation of memory occupied by objects that are no longer reachable from GC roots.
How does Java know an object is no longer needed? An object is considered garbage when it cannot be reached from any GC root through a chain of strong references.
What are GC Roots? Objects that are always considered live: active thread stacks, static fields, JNI references, and JVM internal structures.
What is the difference between minor GC and full GC? Minor GC collects only the young generation; full GC collects the entire heap (and often metadata). Full GC is typically longer.
What is generational garbage collection? A strategy that separates objects by age, collecting the young generation frequently and the old generation less often, based on the observation that most objects die young.
Which Java GC should I use? It depends on your latency and throughput requirements, heap size, and workload. G1 is a safe default; ZGC or Shenandoah for low latency; Parallel GC for batch.
Does garbage collection stop the application? Some phases do. Modern collectors minimize STW pauses, but cannot eliminate them entirely.
Can GC prevent memory leaks? No. It prevents dangling pointers and double frees, but logical leaks (unwanted retention) still occur.
What causes OutOfMemoryError? The heap (or another memory area) is full and the collector cannot reclaim enough memory for the requested allocation. Usually due to a leak, a heap that is too small, or excessive live data.
Does System.gc() force garbage collection?
It is only a hint. The JVM may ignore it. Using it is generally discouraged.
What is the difference between G1, ZGC, and Shenandoah? G1 is a generational, region‑based collector with predictable pauses (default). ZGC and Shenandoah are low‑latency collectors targeting sub‑millisecond pauses, but with higher CPU overhead.
24. Next Steps​
Continue your runtime education with these articles:
- JIT Compiler in Java: From Bytecode to Machine Code – how hot code becomes native.
- Java Concurrency Model: Threads, Locks, and Synchronization – the practical side of concurrency.
- Virtual Threads in Java: Modern Concurrency with Project Loom – lightweight threads that scale.
Then move into Performance Engineering to apply your knowledge:
- JVM Performance Tuning: Memory, GC, and Runtime Optimization
- Java Garbage Collection Tuning Best Practices
- Java Memory Leak Detection and Troubleshooting Guide
- Java Profiling Tools: JFR, JMC, and Async Profiler Guide
25. Key Takeaways​
- Java automatically manages memory through garbage collection, reclaiming objects that are unreachable from GC roots.
- The object lifecycle is allocation → reachable → unreachable → collected; unreachable means eligible, not immediately freed.
- Reachability is determined by graph traversal from GC roots.
- Generational collection leverages the fact that most objects die young; the heap is split into young and old generations.
- Major collectors—Serial, Parallel, G1, ZGC, Shenandoah—offer different trade‑offs between throughput and latency.
- GC pauses are not eliminated by concurrent collectors; they are minimized.
- Garbage collection does not prevent logical memory leaks; an object can be reachable yet useless.
- Effective GC engineering starts with measurement: GC logs, JFR, and heap dumps.
- Choosing a collector is a decision based on workload, latency targets, and resource constraints.
- Master the conceptual model, and the tuning practices in Performance Engineering become natural extensions.
Garbage collection is one of the most misunderstood components of the JVM. With this foundation, you are ready to think clearly about memory behavior, diagnose production issues, and make informed decisions about how your applications use memory.