Garbage Collection
How the JVM Allocates Memory
Section titled “How the JVM Allocates Memory”When you write new MyObject(), the JVM performs several steps to allocate memory. These concepts describe the techniques used for fast allocation and the structure of objects in memory.
Memory Allocation Techniques
Section titled “Memory Allocation Techniques”These are performance optimizations that make the process of allocating memory extremely fast.
-
Thread-Local Allocation Buffers (TLABs):
- What it is: A strategy where each application thread is given its own small, private buffer of memory for creating objects.
- Purpose: To reduce contention. Instead of all threads competing for a shared memory lock on the heap, they can allocate from their private buffer without synchronization. This significantly improves performance in multi-threaded applications.
- How it works: When a thread needs to create an object, it first tries to use its TLAB. If the TLAB is full, the thread requests a new TLAB from the main heap.
-
Bump-the-Pointer Allocation:
- What it is: An extremely fast, low-level allocation technique used within a TLAB (or any contiguous free memory block).
- Purpose: To make the act of allocation incredibly cheap.
- How it works: The JVM maintains a pointer to the next available memory address. To allocate an object, it simply checks if there’s enough space, reserves the memory by moving (“bumping”) the pointer forward by the object’s size, and returns the previous address. This is often just a few machine instructions.
Object Memory Layout
Section titled “Object Memory Layout”These concepts describe the structure and rules governing how an object is physically laid out in memory after it has been allocated.
-
Object Header:
- What it is: A block of metadata that the JVM adds to every object created.
- Purpose: To store essential information for the JVM’s internal operations, such as garbage collection and thread synchronization.
- Contents:
- Mark Word: Stores the object’s synchronization status (lock information), identity hash code, and age (how many GC cycles it has survived).
- Class Metadata Pointer: A pointer to the object’s class definition in the Metaspace.
- Array Length: For array objects, this stores the length of the array.
-
Object Alignment:
- What it is: A rule that dictates that objects must be placed in memory at addresses that are a multiple of a certain number (typically 8 bytes).
- Purpose: To improve CPU performance. Modern processors can access data from memory much faster when it is aligned to their word size.
- How it works: If an object’s size isn’t a multiple of 8 bytes, the JVM will add a few empty bytes of “padding” after it to ensure the next object starts on a correctly aligned boundary. This can lead to minor memory wastage but provides a significant performance gain.
Simple Analogy for memory allocation
Section titled “Simple Analogy for memory allocation”Imagine a factory floor (the JVM Heap) where many workers (threads) need to grab parts (memory for objects) from a central bin.
- The Problem: If all workers run to the same bin, they’ll constantly bump into each other, and you’d need a manager (a lock) to make sure only one worker grabs a part at a time. This is slow.
- The Solution (TLABs): You give each worker their own small, private box of parts right at their workstation. This is a TLAB. Now workers can grab parts without interfering with each other.
- The Technique (Bump-the-Pointer): The parts in the private box are neatly stacked. To get a part, a worker just takes the one off the top. The “pointer” to the top of the stack moves down by one. This is Bump-the-Pointer allocation.
- The Part Itself (Object Header): Every part has a serial number and a quality control stamp on it. This is the Object Header. It doesn’t help the worker grab the part, but it’s essential for tracking the part later.
- The Storage Rule (Object Alignment): The factory has a rule that all part boxes must be placed on shelves at 8-inch intervals for the robotic arms (CPU) to grab them efficiently. This is Object Alignment.
Collection Phases In Detail
Section titled “Collection Phases In Detail”1. Stop-The-World (STW) Pauses
Section titled “1. Stop-The-World (STW) Pauses”A Stop-The-World (STW) pause is when the JVM freezes all application threads to safely perform garbage collection.
- Why is it necessary? The GC needs a stable snapshot of memory. If application threads were allowed to run, they could create new objects or change object references while the GC is trying to identify live objects, leading to an inconsistent state and incorrect collections.
- Impact: During a pause, the application is completely unresponsive. The duration of these pauses is a critical factor in application performance and latency. A key goal of modern garbage collectors (like G1, ZGC, and Shenandoah) is to minimize the length and frequency of these pauses.
2. Minor Garbage Collection (Young Generation)
Section titled “2. Minor Garbage Collection (Young Generation)”A Minor GC is triggered when the Eden space becomes full. It is designed to be fast because it operates on a simple assumption: most newly created objects die young.
The process follows these steps:
- Trigger: An application thread tries to allocate a new object, but Eden is full. This triggers a Minor GC.
- STW Pause: All application threads are paused.
- Root Scanning: The GC identifies all “live” objects by starting from GC Roots (e.g., active thread stacks, static variables, JNI references) and traversing the object graph.
- Copying Live Objects:
- All live objects in Eden are copied to the currently empty survivor space (the “To” space).
- All live objects in the other survivor space (the “From” space, which contains objects from previous Minor GCs) are also copied to the “To” space.
- Age Incrementation: As each object is copied to the “To” space, its age counter (stored in the object header) is incremented. This counter tracks how many GC cycles an object has survived.
- Clearing: After all live objects have been copied, the Eden and “From” survivor spaces are now considered empty. The GC can clear them instantly without iterating through them, which is extremely fast.
- Role Swap: The “To” and “From” survivor spaces swap their roles. The space that was just filled becomes the new “From” space, and the one that was just cleared becomes the new “To” space for the next Minor GC.
- Resume: The STW pause ends, and application threads resume.
3. Major Garbage Collection (Full GC - Old Generation)
Section titled “3. Major Garbage Collection (Full GC - Old Generation)”A Major GC (often part of a Full GC) cleans the Old Generation. It is a much more expensive and time-consuming operation because it deals with a larger set of objects and often involves more complex algorithms.
It is typically triggered when:
- The Old Generation is running out of space.
- The Metaspace (where class metadata is stored) is full.
- A developer makes an explicit call to
System.gc()(this is a suggestion to the JVM, not a command, and is highly discouraged in production).
A common algorithm for Major GC is Mark-Sweep-Compact:
- Mark Phase: The GC identifies all live objects in the Old Generation by traversing the object graph from the GC Roots. This is a more complex process than in a Minor GC because it involves scanning the entire Old Generation.
- Sweep Phase: The GC scans the entire Old Generation and reclaims the memory used by any object that was not marked as live. These memory locations are now considered free.
- Compact Phase: After the sweep phase, the memory in the Old Generation can be fragmented (like a hard drive with files deleted all over the place). The compact phase moves all the remaining live objects together into a contiguous block at the beginning of the Old Generation. This eliminates fragmentation and makes future allocations much faster.
The Object Lifecycle: Promotion from Young to Old
Section titled “The Object Lifecycle: Promotion from Young to Old”The journey of a typical long-lived object looks like this:
- Birth: The object is created in the Eden space.
- First Survival: A Minor GC occurs. The object is found to be live and is copied to a Survivor Space (S0). Its age is incremented to 1.
- Continued Survival: At the next Minor GC, the object is still live. It is copied from S0 to the other Survivor Space (S1). Its age is incremented to 2.
- Bouncing: The object continues to “bounce” between S0 and S1 with each subsequent Minor GC, its age increasing each time.
- Promotion: The object’s age eventually reaches the Tenuring Threshold (a configurable value, often defaulting to 15 in the HotSpot JVM). At the next Minor GC it survives, instead of being copied to the other survivor space, it is promoted to the Old Generation.
Promotion Thresholds and Adaptive Sizing
Section titled “Promotion Thresholds and Adaptive Sizing”- Tenuring Threshold (
-XX:MaxTenuringThreshold): This JVM flag controls the age at which an object is promoted. Setting it too low can cause short-lived objects to be promoted prematurely, filling the Old Generation and causing more frequent Full GCs. Setting it too high may cause objects to linger in the survivor spaces unnecessarily. - Adaptive Sizing (
-XX:+UseAdaptiveSizePolicy): Modern JVMs are intelligent. With adaptive sizing enabled (which is the default for most collectors), the JVM can dynamically adjust the sizes of the Eden and survivor spaces, and even the tenuring threshold itself, based on the application’s actual memory allocation patterns. The goal is to meet performance targets, such as minimizing pause times or maximizing throughput.
Garbage Collection Tuning Parameters
Section titled “Garbage Collection Tuning Parameters”Key Heap Size Parameters
Section titled “Key Heap Size Parameters”-Xms: Sets the initial heap size when the JVM starts.-Xmx: Sets the maximum heap size the JVM can grow to.
It’s a common best practice to set -Xms and -Xmx to the same value in production environments to prevent pauses caused by the heap resizing during runtime.
How Heap Memory is Divided (The Ratios)
Section titled “How Heap Memory is Divided (The Ratios)”The allocation of memory into Eden, Survivor, and Old Generation spaces is based on a set of default ratios, which can be overridden by other JVM flags.
1. Division between Young and Old Generation
- Governing Flag:
-XX:NewRatio - Default Value:
2 - Meaning: This ratio controls the size of the Old Generation relative to the Young Generation. A
NewRatioof2means Old Generation : Young Generation = 2 : 1. The total heap is effectively divided into 3 parts (2 for Old, 1 for Young).
2. Division within the Young Generation
- Governing Flag:
-XX:SurvivorRatio - Default Value:
8 - Meaning: This ratio controls the size of the Eden space relative to a single Survivor space. A
SurvivorRatioof8means Eden : S0 = 8 : 1 and Eden : S1 = 8 : 1. The Young Generation is thus divided into 10 parts (8 for Eden, 1 for S0, 1 for S1).
Example: -Xms1GB and -Xmx3GB
Section titled “Example: -Xms1GB and -Xmx3GB”With this configuration, the JVM starts with a 1GB heap. Here is the initial memory layout based on the default ratios:
- Initial Heap: 1GB (1024 MB)
- Young Generation Size:
1024 MB / 3= ~341 MB - Old Generation Size:
2 * (1024 MB / 3)= ~683 MB
And within the Young Generation:
- Eden Size:
(341 MB / 10) * 8= ~273 MB - S0 Size:
(341 MB / 10) * 1= ~34 MB - S1 Size:
(341 MB / 10) * 1= ~34 MB
The Role of Adaptive Sizing
Section titled “The Role of Adaptive Sizing”It’s crucial to know that this initial layout is not static. The Adaptive Size Policy (-XX:+UseAdaptiveSizePolicy), which is enabled by default in most modern GCs, allows the JVM to dynamically change these ratios and generation sizes at runtime. The JVM will monitor the application’s behavior and adjust the sizes to meet performance goals, such as minimizing pause times. As the heap grows from the initial 1GB towards the maximum 3GB, all generations will grow and adapt accordingly.
GC Algorithm Selection:
Section titled “GC Algorithm Selection:”XX:+UseSerialGC: Serial collectorXX:+UseParallelGC: Parallel collectorXX:+UseConcMarkSweepGC: CMS collector (deprecated)XX:+UseG1GC: G1 collector (default in modern JVMs)XX:+UseZGC: Z Garbage Collector (Java 11+)XX:+UseShenandoahGC: Shenandoah GC (Java 12+)
Recent Advancements (Java 11+)
Section titled “Recent Advancements (Java 11+)”- ZGC (Z Garbage Collector):
- Designed for very large heaps (terabytes)
- Sub-millisecond pause times
- Concurrent processing (marking, relocation, compaction)
- Shenandoah GC:
- Ultra-low pause times
- Concurrent evacuation of live objects
- Trades CPU cycles for reduced pauses
Generational Model in Modern Garbage Collectors
Section titled “Generational Model in Modern Garbage Collectors”The traditional model of physically separate Young and Old generations starts to change with modern Garbage Collectors.
- G1GC: Yes, but logically, not physically. It still uses the concept of generations.
- ZGC & Shenandoah: No, not in the traditional sense. They were originally single-generation, but have now re-introduced generational modes as a key optimization.
Here is a detailed breakdown of how each one works.
1. G1 (Garbage-First) GC
Section titled “1. G1 (Garbage-First) GC”G1 does not have physically contiguous blocks of memory for Eden, Survivor, and Old spaces. Instead, it partitions the entire heap into a large number of small, equal-sized regions (typically 1MB to 32MB).
- Heap Layout: A collection of regions.
- Generational Concept: Yes, logically. Each region is tagged at runtime as being part of the Young Generation (Eden or Survivor) or the Old Generation.
- How it Works:
- New objects are allocated in regions designated as “Eden”.
- When Eden regions fill up, G1 performs a “Young-Only” collection. Live objects are copied to a new set of regions that become the Survivor space.
- Objects are still aged and promoted to Old regions after reaching the tenuring threshold.
- The key advantage is that G1 can select a set of regions (the “collection set”) that contain the most garbage to collect, allowing it to meet specific pause-time goals. This set usually contains young regions but can also include old regions.
- Humongous Objects: Objects larger than 50% of a region’s size are allocated directly into special “Humongous” regions within the Old generation.
In short: G1 still believes in the generational hypothesis but implements it in a more flexible, non-contiguous, region-based way.
2. ZGC and Shenandoah GC
Section titled “2. ZGC and Shenandoah GC”ZGC and Shenandoah prioritize extremely low pause times (often sub-millisecond) by doing almost all their work concurrently with the application threads.
- Heap Layout: Also region-based, similar to G1.
- Generational Concept: Originally no, but now yes.
- Original Design: They were designed as single-generation collectors, treating the entire heap as one big space. This simplified the collector but was less efficient as it couldn’t assume most objects die young.
- Recent Evolution (Java 21+): Both ZGC and Shenandoah have introduced Generational modes. By re-introducing a Young Generation, they can collect the vast majority of garbage (short-lived objects) with much less CPU overhead, leading to better application throughput while still maintaining ultra-low pause times.
- Key Innovations: They use advanced techniques like colored pointers (ZGC) and forwarding pointers (Shenandoah) to move objects (compact memory) concurrently while application threads are running. This is their “magic” for avoiding long STW pauses.
Summary Table
Section titled “Summary Table”| Garbage Collector | Heap Layout | Generational? | Key Characteristic |
|---|---|---|---|
| Parallel / Serial | Contiguous blocks for Eden, Survivor, and Old. | Yes (Strictly) | Classic generational model. Requires long “Stop-The-World” pauses for full compaction. |
| G1GC | Heap divided into many small, equal-sized regions. | Yes (Logically) | Regions are tagged as Eden, Survivor, or Old. Collects regions with the most garbage first. |
| ZGC / Shenandoah | Heap divided into regions. | No (originally). Yes (in modern JVMs). | Performs nearly all work concurrently, achieving ultra-low pause times. |
ZGC vs. Shenandoah: A Deeper Dive
Section titled “ZGC vs. Shenandoah: A Deeper Dive”While ZGC and Shenandoah share the same goal—ultra-low pause times—they achieve it through different internal strategies. Understanding these differences is key to choosing the right one.
Core Technical Differences
Section titled “Core Technical Differences”The main distinction lies in how they handle concurrent object relocation.
-
ZGC (Z Garbage Collector):
- Technique: Uses Colored Pointers and Load Barriers.
- How it Works: ZGC uses some of the bits within a 64-bit pointer to store metadata (or “color”). When your application code tries to access an object, a tiny, highly-optimized load barrier checks the pointer’s color. If the color indicates the object has been moved, the barrier transparently updates the pointer to the new location before your application uses it.
-
Shenandoah GC:
- Technique: Uses Forwarding Pointers and Read/Write Barriers.
- How it Works: When Shenandoah moves an object, it leaves behind a forwarding pointer at the object’s old address. When your application tries to access an object, a barrier checks if a forwarding pointer exists. If it does, it updates your application’s reference to the new location and then completes the access.
Analogy: Imagine a city is renaming all its streets.
- ZGC’s approach (Colored Pointers) is like giving everyone a “smart map”. When you look up a street, the map automatically tells you, “That street is now called ‘X’, here’s the new location.”. Your map is always up-to-date the moment you use it.
- Shenandoah’s approach (Forwarding Pointers) is like putting up a sign at the end of the old street that says, “This street has been renamed. Please go to ‘X’. You have to go to the old location first to find the new one, but you eventually get there.
When to Use Which?
Section titled “When to Use Which?”The choice between ZGC and Shenandoah depends on your specific priorities, as they make slightly different trade-offs.
Use ZGC When:
Section titled “Use ZGC When:”- Lowest Possible Latency is Your #1 Priority: ZGC is designed to be uncompromising on pause times. If your application’s absolute top priority is to avoid pauses, even at the cost of slightly lower overall throughput, ZGC is often the better choice.
- You Have an Extremely Large Heap: ZGC was built from the ground up to handle multi-terabyte heaps. Its performance characteristics are designed to be independent of heap size.
- You Prefer Simplicity and Fewer Knobs: ZGC aims to be a “just works” collector with minimal tuning required.
- You Use Standard Oracle OpenJDK: ZGC is developed and fully supported by Oracle as a primary feature.
Use Shenandoah When:
Section titled “Use Shenandoah When:”- You Need a Balance Between Low Latency and High Throughput: Shenandoah is often seen as providing a better balance. It can sometimes achieve higher application throughput than ZGC because its barrier mechanism can be less intrusive, at the cost of potentially slightly longer (but still ultra-low) pause times.
- You Want More Tuning Control: Shenandoah offers more configuration options, giving you finer control over the GC’s heuristics and its trade-offs between CPU usage, memory footprint, and pause times.
- Your Heap is Large, but Not Necessarily Massive: While it scales very well, Shenandoah is often considered a great fit for a wider range of application sizes, not just the extreme high-end.
- You Use a Red Hat or Other Community OpenJDK Build: Shenandoah was developed by Red Hat and has a long history of strong support in non-Oracle OpenJDK distributions.
Summary
| Factor | ZGC | Shenandoah |
|---|---|---|
| Primary Goal | Uncompromisingly low pause times | Balance of low pause time & throughput |
| Core Technique | Colored Pointers & Load Barriers | Forwarding Pointers & Read/Write Barriers |
| Ideal Use Case | Latency-critical apps, massive heaps | Latency-sensitive apps needing throughput |
| Tuning Philosophy | Minimal tuning, self-managing | More configurable, finer control |
| Primary Backer | Oracle | Red Hat |
In modern Java (21+), both are excellent, state-of-the-art collectors. The best choice often comes down to benchmarking your specific application with both to see which one provides the better performance profile.
GC Monitoring and Diagnostic Flags
Section titled “GC Monitoring and Diagnostic Flags”- Useful Diagnostic Flags:
XX:+PrintGCDetails: Print detailed GC logsXX:+PrintGCDateStamps: Add timestamps to GC logsXX:+HeapDumpOnOutOfMemoryError: Create heap dump on OOM
- Performance Monitoring:
- JConsole, JVisualVM, Java Mission Control for real-time monitoring
- GC logs analysis with tools like GCViewer
Common Interview Questions
Section titled “Common Interview Questions”- How does GC determine if an object is garbage?
- Objects unreachable from any GC root are considered garbage
- GC roots include: thread stacks, static fields, JNI references
- What is a memory leak in Java?
- Objects remaining reachable but never used again
- Common causes: unclosed resources, improper caching, forgotten listeners
- How to handle OutOfMemoryError?
- Analyze heap dumps
- Check for memory leaks
- Adjust heap size parameters
- Optimize application code
- What’s the difference between finalize() and PhantomReference?
- finalize(): Unpredictable, deprecated method for cleanup
- PhantomReference: Safer, more deterministic alternative for resource cleanup
- Weak, Soft, and Phantom References:
- WeakReference: Collected when only weakly reachable
- SoftReference: Collected before OutOfMemoryError (memory-sensitive caches)
- PhantomReference: Collected but provides notification before removal
- G1 vs CMS Collector:
- G1: Region-based, predictable pauses, default in modern Java
- CMS: Concurrent but fragmentation-prone, deprecated in Java 9+