Java Virtual Machine Garbage Collection - Complete Guide
Memory Management Fundamentals
Java’s automatic memory management through garbage collection is one of its key features that differentiates it from languages like C and C++. The JVM automatically handles memory allocation and deallocation, freeing developers from manual memory management while preventing memory leaks and dangling pointer issues.
Memory Layout Overview
The JVM heap is divided into several regions, each serving specific purposes in the garbage collection process:
flowchart TB
subgraph "JVM Memory Structure"
subgraph "Heap Memory"
subgraph "Young Generation"
Eden["Eden Space"]
S0["Survivor 0"]
S1["Survivor 1"]
end
subgraph "Old Generation"
OldGen["Old Generation (Tenured)"]
end
MetaSpace["Metaspace (Java 8+)"]
end
subgraph "Non-Heap Memory"
PC["Program Counter"]
Stack["Java Stacks"]
Native["Native Method Stacks"]
Direct["Direct Memory"]
end
end
Interview Insight: “Can you explain the difference between heap and non-heap memory in JVM?”
Answer: Heap memory stores object instances and arrays, managed by GC. Non-heap includes method area (storing class metadata), program counter registers, and stack memory (storing method calls and local variables). Only heap memory is subject to garbage collection.
GC Roots and Object Reachability
Understanding GC Roots
GC Roots are the starting points for garbage collection algorithms to determine object reachability. An object is considered “reachable” if there’s a path from any GC Root to that object.
Primary GC Roots include:
- Local Variables: Variables in currently executing methods
- Static Variables: Class-level static references
- JNI References: Objects referenced from native code
- Monitor Objects: Objects used for synchronization
- Thread Objects: Active thread instances
- Class Objects: Loaded class instances in Metaspace
flowchart TD
subgraph "GC Roots"
LV["Local Variables"]
SV["Static Variables"]
JNI["JNI References"]
TO["Thread Objects"]
end
subgraph "Heap Objects"
A["Object A"]
B["Object B"]
C["Object C"]
D["Object D (Unreachable)"]
end
LV --> A
SV --> B
A --> C
B --> C
style D fill:#ff6b6b
style A fill:#51cf66
style B fill:#51cf66
style C fill:#51cf66
Object Reachability Algorithm
The reachability analysis works through a mark-and-sweep approach:
- Mark Phase: Starting from GC Roots, mark all reachable objects
- Sweep Phase: Reclaim memory of unmarked (unreachable) objects
1 | // Example: Object Reachability |
Interview Insight: “How does JVM determine if an object is eligible for garbage collection?”
Answer: JVM uses reachability analysis starting from GC Roots. If an object cannot be reached through any path from GC Roots, it becomes eligible for GC. This is more reliable than reference counting as it handles circular references correctly.
Object Reference Types
Java provides different reference types that interact with garbage collection in distinct ways:
Strong References
Default reference type that prevents garbage collection:
1 | Object obj = new Object(); // Strong reference |
Weak References
Allow garbage collection even when references exist:
1 | import java.lang.ref.WeakReference; |
Soft References
More aggressive than weak references, collected only when memory is low:
1 | import java.lang.ref.SoftReference; |
Phantom References
Used for cleanup operations, cannot retrieve the object:
1 | import java.lang.ref.PhantomReference; |
Interview Insight: “When would you use WeakReference vs SoftReference?”
Answer: Use WeakReference for cache entries that can be recreated easily (like parsed data). Use SoftReference for memory-sensitive caches where you want to keep objects as long as possible but allow collection under memory pressure.
Generational Garbage Collection
The Generational Hypothesis
Most objects die young - this fundamental observation drives generational GC design:
flowchart LR
subgraph "Object Lifecycle"
A["Object Creation"] --> B["Short-lived Objects (90%+)"]
A --> C["Long-lived Objects (<10%)"]
B --> D["Die in Young Generation"]
C --> E["Promoted to Old Generation"]
end
Young Generation Structure
Eden Space: Where new objects are allocated
Survivor Spaces (S0, S1): Hold objects that survived at least one GC cycle
1 | // Example: Object allocation flow |
Minor GC Process
- Allocation: New objects go to Eden
- Eden Full: Triggers Minor GC
- Survival: Live objects move to Survivor space
- Age Increment: Survivor objects get age incremented
- Promotion: Old enough objects move to Old Generation
sequenceDiagram
participant E as Eden Space
participant S0 as Survivor 0
participant S1 as Survivor 1
participant O as Old Generation
E->>S0: First GC: Move live objects
Note over S0: Age = 1
E->>S0: Second GC: New objects to S0
S0->>S1: Move aged objects
Note over S1: Age = 2
S1->>O: Promotion (Age >= threshold)
Major GC and Old Generation
Old Generation uses different algorithms optimized for long-lived objects:
- Concurrent Collection: Minimize application pause times
- Compaction: Reduce fragmentation
- Different Triggers: Based on Old Gen occupancy or allocation failure
Interview Insight: “Why is Minor GC faster than Major GC?”
Answer: Minor GC only processes Young Generation (smaller space, most objects are dead). Major GC processes entire heap or Old Generation (larger space, more live objects), often requiring more complex algorithms like concurrent marking or compaction.
Garbage Collection Algorithms
Mark and Sweep
The fundamental GC algorithm:
Mark Phase: Identify live objects starting from GC Roots
Sweep Phase: Reclaim memory from dead objects
flowchart TD
subgraph "Mark Phase"
A["Start from GC Roots"] --> B["Mark Reachable Objects"]
B --> C["Traverse Reference Graph"]
end
subgraph "Sweep Phase"
D["Scan Heap"] --> E["Identify Unmarked Objects"]
E --> F["Reclaim Memory"]
end
C --> D
Advantages: Simple, handles circular references
Disadvantages: Stop-the-world pauses, fragmentation
Copying Algorithm
Used primarily in Young Generation:
1 | // Conceptual representation |
Advantages: No fragmentation, fast allocation
Disadvantages: Requires double memory, inefficient for high survival rates
Mark-Compact Algorithm
Combines marking with compaction:
- Mark: Identify live objects
- Compact: Move live objects to eliminate fragmentation
flowchart LR
subgraph "Before Compaction"
A["Live"] --> B["Dead"] --> C["Live"] --> D["Dead"] --> E["Live"]
end
flowchart LR
subgraph "After Compaction"
F["Live"] --> G["Live"] --> H["Live"] --> I["Free Space"]
end
Interview Insight: “Why doesn’t Young Generation use Mark-Compact algorithm?”
Answer: Young Generation has high mortality rate (90%+ objects die), making copying algorithm more efficient. Mark-Compact is better for Old Generation where most objects survive and fragmentation is a concern.
Incremental and Concurrent Algorithms
Incremental GC: Breaks GC work into small increments
Concurrent GC: Runs GC concurrently with application threads
1 | // Tri-color marking for concurrent GC |
Garbage Collectors Evolution
Serial GC (-XX:+UseSerialGC)
Characteristics: Single-threaded, stop-the-world
Best for: Small applications, client-side applications
JVM Versions: All versions
1 | # JVM flags for Serial GC |
Use Case Example:
1 | // Small desktop application |
Parallel GC (-XX:+UseParallelGC)
Characteristics: Multi-threaded, throughput-focused
Best for: Batch processing, throughput-sensitive applications
Default: Java 8 (server-class machines)
1 | # Parallel GC configuration |
Production Example:
1 | // Data processing application |
CMS GC (-XX:+UseConcMarkSweepGC) [Deprecated in Java 14]
Phases:
- Initial Mark (STW)
- Concurrent Mark
- Concurrent Preclean
- Remark (STW)
- Concurrent Sweep
Characteristics: Concurrent, low-latency focused
Best for: Web applications requiring low pause times
1 | # CMS configuration (legacy) |
G1 GC (-XX:+UseG1GC)
Characteristics: Low-latency, region-based, predictable pause times
Best for: Large heaps (>4GB), latency-sensitive applications
Default: Java 9+
1 | # G1 GC tuning |
Region-based Architecture:
flowchart TB
subgraph "G1 Heap Regions"
subgraph "Young Regions"
E1["Eden 1"]
E2["Eden 2"]
S1["Survivor 1"]
end
subgraph "Old Regions"
O1["Old 1"]
O2["Old 2"]
O3["Old 3"]
end
subgraph "Special Regions"
H["Humongous"]
F["Free"]
end
end
Interview Insight: “When would you choose G1 over Parallel GC?”
Answer: Choose G1 for applications requiring predictable low pause times (<200ms) with large heaps (>4GB). Use Parallel GC for batch processing where throughput is more important than latency.
ZGC (-XX:+UseZGC) [Java 11+]
Characteristics: Ultra-low latency (<10ms), colored pointers
Best for: Applications requiring consistent low latency
1 | # ZGC configuration |
Shenandoah GC (-XX:+UseShenandoahGC) [Java 12+]
Characteristics: Low pause times, concurrent collection
Best for: Applications with large heaps requiring consistent performance
1 | # Shenandoah configuration |
Collector Comparison
Collector Comparison Table:
Collector | Java Version | Best Heap Size | Pause Time | Throughput | Use Case |
---|---|---|---|---|---|
Serial | All | < 100MB | High | Low | Single-core, client apps |
Parallel | All (default 8) | < 8GB | Medium-High | High | Multi-core, batch processing |
G1 | 7+ (default 9+) | > 4GB | Low-Medium | Medium-High | Server applications |
ZGC | 11+ | > 8GB | Ultra-low | Medium | Latency-critical applications |
Shenandoah | 12+ | > 8GB | Ultra-low | Medium | Real-time applications |
GC Tuning Parameters and Best Practices
Heap Sizing Parameters
1 | # Basic heap configuration |
Young Generation Tuning
1 | # Young generation specific tuning |
Real-world Example:
1 | // Web application tuning scenario |
Monitoring and Logging
1 | # GC logging (Java 8) |
Production Tuning Checklist
Memory Allocation:
1 | // Monitor allocation patterns |
GC Overhead Analysis:
1 | // Acceptable GC overhead typically < 5% |
Advanced GC Concepts
Escape Analysis and TLAB
Thread Local Allocation Buffers (TLAB) optimize object allocation:
1 | public class TLABExample { |
String Deduplication (G1)
1 | # Enable string deduplication |
1 | // String deduplication example |
Compressed OOPs
1 | # Enable compressed ordinary object pointers (default on 64-bit with heap < 32GB) |
Interview Questions and Advanced Scenarios
Scenario-Based Questions
Question: “Your application experiences long GC pauses during peak traffic. How would you diagnose and fix this?”
Answer:
- Analysis: Enable GC logging, analyze pause times and frequency
- Identification: Check if Major GC is causing long pauses
- Solutions:
- Switch to G1GC for predictable pause times
- Increase heap size to reduce GC frequency
- Tune young generation size
- Consider object pooling for frequently allocated objects
1 | // Example diagnostic approach |
Question: “Explain the trade-offs between throughput and latency in GC selection.”
Answer:
- Throughput-focused: Parallel GC maximizes application processing time
- Latency-focused: G1/ZGC minimizes pause times but may reduce overall throughput
- Choice depends on: Application requirements, SLA constraints, heap size
Memory Leak Detection
1 | // Common memory leak patterns |
Production Best Practices
Monitoring and Alerting
1 | // JMX-based GC monitoring |
Capacity Planning
1 | // Capacity planning calculations |
Performance Testing
1 | // GC performance testing framework |
Conclusion and Future Directions
Java’s garbage collection continues to evolve with new collectors like ZGC and Shenandoah pushing the boundaries of low-latency collection. Understanding GC fundamentals, choosing appropriate collectors, and proper tuning remain critical for production Java applications.
Key Takeaways:
- Choose GC based on application requirements (throughput vs latency)
- Monitor and measure before optimizing
- Understand object lifecycle and allocation patterns
- Use appropriate reference types for memory-sensitive applications
- Regular capacity planning and performance testing
Future Trends:
- Ultra-low latency collectors (sub-millisecond pauses)
- Better integration with container environments
- Machine learning-assisted GC tuning
- Region-based collectors becoming mainstream
The evolution of GC technology continues to make Java more suitable for a wider range of applications, from high-frequency trading systems requiring microsecond latencies to large-scale data processing systems prioritizing throughput.