Last Updated on May 22, 2024
Introduced in Java 11, the Z Garbage Collector (ZGC) has emerged as a compelling option for low-latency garbage collection in Java, particularly gaining traction with the recent advancements in Java 21 generational ZGC.
The garbage collector (GC) cleans up unused objects, freeing up memory for new ones. But just keeping track of free space isn’t enough. Over time, memory becomes fragmented (scattered) as objects are created and deleted. To prevent this, the JVM might also compact memory, rearranging things to create larger contiguous blocks of free space for future allocations.
While the details can get technical, garbage collection (GC) boils down to three key tasks: finding unused objects, freeing their memory, and organizing that free memory.
Different GC algorithms handle these tasks in unique ways, especially when it comes to organizing memory. Some wait until absolutely necessary to reorganize, while others tackle it in larger chunks or by moving small bits at a time. These different approaches are what make some GC algorithms faster or slower for specific situations.
One tricky aspect of GC is that sometimes it needs to move objects around in memory. This can be a problem because if an application thread is trying to use an object at the same time it’s being moved, things can go wrong. To prevent this, GC pauses all application threads for a short moment while it relocates objects.
This is known as stop-the-world pauses.
Most garbage collectors organize memory in a similar way, even though the specifics might vary slightly.
They typically split the memory area used by objects (called the heap) into different sections based on how long objects have typically lived. These sections are called generations.These are called the old (or tenured) generation and the young generation. The young generation is further divided into sections known as eden and the survivor spaces The rationale for having separate generations is that many objects are used for a very short period of time.
The CMS collector
The CMS collector was the first concurrent collector. Like other algorithms, CMS stops all application threads during a minor GC, which it performs with multiple Threads.
The Concurrent Mark Sweep (CMS) collector (also referred to as the concurrent low pause collector) collects the tenured generation. It attempts to minimize the pauses due to garbage collection by doing most of the garbage collection work concurrently with the application threads. Normally the concurrent low pause collector does not copy or compact the live objects. A garbage collection is done without moving the live objects. If fragmentation becomes a problem, we need to allocate a larger heap.
It aimed to minimize pauses during garbage collection cycles, making it suitable for applications that require low latency (minimal lag or delay). Instead of stopping all application threads CMS attempted to run concurrently with application threads. It identified unused objects (marking) while the application continued to run. Later, in a separate stop-the-world pause (sweeping), it reclaimed the memory occupied by those unused objects.
One of the limitations of CMS was so called “floating garbage”, namely, marking and sweeping happened at different times, some objects might become unreachable during the application’s execution but still be marked as reachable during the initial marking phase, which weren’t reclaimed until the next collection cycle.
In addition to this, running garbage collection concurrently with application threads could consume more processing power.
The Garbage First(G1) Garbage Collector
G1 GC, or Garbage-First Garbage Collector, is a relatively new type of garbage collector introduced in Java 7 Update 4 and became the default collector in Java 9. It’s designed to address some limitations of previous collectors and offer several advantages:
The memory area used by objects (called the heap) is divided into smaller regions. Each region can be part of the young generation (for new objects) or the old generation (for long-lived objects).
G1 prioritizes collecting regions in the young generation first, as they tend to have more garbage. However, it can also collect parts of the old generation if necessary. This allows G1 to focus on collecting only the regions that are most likely to contain garbage, improving efficiency.
While G1 primarily works concurrently with application threads (meaning it can run garbage collection tasks while your program is still running), it might use brief stop-the-world pauses in specific situations. These pauses are typically much shorter than with older collectors.
The Z Garbage Collector i.e. Java 21 generational ZGC
ZGC, or Z Garbage Collector, is a relatively new and experimental collector introduced in Java 11 (JEP 333), as an optional feature. It became production-ready in Java 15 and boasts some impressive capabilities:
ZGC is built for applications that need lightning-fast performance. It keeps pauses below 10 milliseconds and can handle massive amounts of memory, making it ideal for real-time systems and big data processing.
ZGC is not the default collector in Java and needs to be explicitly enabled (-XX:+UseZGC.). The most important tuning option for ZGC is setting the max heap size (-Xmx)
ZGC generally performs better with more memory. However, it’s important to strike a balance and avoid wasting resources.
Beyond setting the memory size, ZGC gives you some control over its cleaning process. You can adjust the number of concurrent garbage collection threads (using the -XX:ConcGCThreads option) to potentially fine-tune performance.
You can adjust how much processing power the GC uses. Too much CPU time for the GC can slow down your application, while too little can lead to a buildup of unused data. It’s about finding the right balance.
With Java 21, it has evolved into a generational GC (JEP 439).
To use Java 21 generational ZGC requires passing two VM arguments
-XX:+UseZGC -XX:+ZGenerational
The Java 21 Generational ZGC aims to improve application performance, extending the existing ZGC by maintaining separate generations for young and old objects.
The goal is to add these benefits on top of what the non-generational approach already offers: lightning-fast pauses (less than a millisecond!), support for incredibly large heaps (think terabytes!), and minimal setup required.
Important design concepts that distinguish Generational ZGC from non-generational ZGC, and from other garbage collectors:
No multi-mapped memory
Classic ZGC employs a technique called multi-mapping where multiple virtual memory ranges map to the same physical memory range.
Java 21 Generational ZGC relied on a traditional approach where each virtual memory range in the heap had a one-to-one correspondence with a physical memory range.
Optimized barriers
Store barriers are a fundamental concept in garbage collection (GC). They are small pieces of code inserted by the compiler or the runtime system during program execution. Their purpose is to ensure that the garbage collector has a consistent view of the memory used by your program’s objects.
Generational ZGC (ZGC) employs several techniques to optimize store barriers, aiming to minimize their performance impact while maintaining accurate garbage collection.
Double-buffered remembered sets
Double-buffered remembered sets are a specific optimization technique used in garbage collection, particularly within Z Garbage Collector (ZGC) introduced in Java. They play a crucial role in efficiently tracking references between generations of objects and minimizing the overhead associated with store barriers.
Relocations without additional heap memory
This refers to a technique that allows the GC to move (or relocate) objects within the existing heap space during a collection cycle without needing to allocate additional memory. This is particularly beneficial for improving efficiency and reducing pauses in low-latency garbage collection scenarios.
Dense heap regions
These are regions in the heap that contain a high proportion of live objects (objects that are still being used by the program). They are essentially “packed” with objects that haven’t been garbage collected yet.
During garbage collection cycles, ZGC analyzes the density of each heap region. This analysis helps ZGC decide the most efficient way to handle each region.
Large objects
ZGC already handles large objects well.In Generational ZGC takes this a step further by allowing large objects to be allocated in the young generation. Given that regions can be aged without relocating them, there is no need to allocate large objects in the old generation just to prevent expensive relocations. Instead, they can be collected in the young generation if they are short-lived or be cheaply promoted to the old generation if they are long-lived.
References between generations
Generational ZGC avoids constantly tracking young-to-old references by piggybacking on young generation collections. This combined approach efficiently identifies and preserves objects still needed by the program across generations.
How to Choose Your JVM GC
Choosing the right garbage collector (GC) for your Java application involves understanding your specific needs and the characteristics of different GC algorithms offered by the JVM.
In most cases, G1GC is a good starting point as it offers a balance between performance and pause times.
If your primary concern is high memory throughput, and pauses are acceptable, Parallel GC might be a good choice. For ultra-low latency requirements, explore ZGC (consider its experimental nature).
You can fine-tune the behavior of some GC algorithms using JVM flags. However, it’s generally recommended to start with default settings and adjust only if necessary
Use profiling tools to analyze your application’s memory usage patterns and GC behavior.
This can help you identify potential bottlenecks and refine your GC selection.