Java 21 debuted in September 2023, along with the introduction of the Generational ZGC. Building on the efficiency of the Z Garbage Collector, this update focuses on optimizing memory management by introducing separate generations for young and old objects.
In this article, we’ll closely examine this addition, exploring its potential benefits, how it works, and how to use it.
2. Garbage Collection
To begin our exploration, let’s delve into the realm of memory management. Garbage collection is the process by which programs try to free up allocated memory that is no longer used by objects. An object is considered ‘in-use’ or ‘referenced’ if some part of our program still maintains a pointer to it. Conversely, an ‘unused’ or ‘unreferenced’ object is no longer accessed by any part of our program, allowing the memory it occupies to be reclaimed.
For example, in Java, the garbage collectors are responsible for freeing up the heap memory, which is where Java objects are stored.
This helps prevent memory leaks and ensures efficient resource usage. It also frees us from having to manually manage the program’s memory, which can lead to potential bugs. Some programming languages, such as Java or C#, come with this feature built-in, while others, like C or C++, may rely on external libraries for similar functionality.
3. Generational Garbage Collection
In the context of memory management, a generation refers to a categorization of objects based on the time of their allocation.
Let’s shift our focus to generational garbage collection. This represents a memory management strategy and works by dividing the objects into different generations, based on the time of allocation, and applying different approaches based on their generation.
In the context of Java, the memory is partitioned into two main generations: young and old. Newly created objects find their place in the young generation, where frequent garbage collection takes place. Objects that persist beyond multiple garbage collection cycles are promoted to the older generation. This division optimizes efficiency by acknowledging the short lifespan of most of the objects.
For more information regarding the generational garbage collection in Java, see the article Java Garbage Collection Basics.
4. The Z Garbage Collector
The Z Garbage Collector, also known as ZGC, is a scalable, low-latency garbage collector. It was first introduced in Java 11 as an experimental feature and became production-ready in Java 15.
The purpose of this feature was to minimize or eliminate long garbage collection pauses, thereby enhancing application responsiveness and accommodating the growing memory capacities of modern systems.
As a non-generational approach, it stores all objects together, regardless of age, so each cycle collects all objects.
5. The Generational Z Garbage Collector
The Generational ZGC aims to improve application performance, extending the existing ZGC by maintaining separate generations for young and old objects.
For most use cases, ZGC is enough to solve latency problems related to garbage collection. This works well as long as there are enough resources available to ensure that the garbage collector can reclaim memory faster than our program consumes it.
However, the weak generational hypothesis states that most objects die young. Consequently, collecting and reclaiming memory from these short-lived young resources requires fewer computational resources. This process unlocks more memory quickly.
On the other hand, collecting older objects, which have survived multiple cycles and have a more extended lifespan, demands more computational resources. However, the amount of memory unlocked by collecting older objects is comparatively less. This strategy proves more efficient in quickly freeing up memory, contributing to enhanced overall application performance.
The Generational ZGC aims to deliver some key advantages compared to the non-generational ZGC:
- reduced risks of allocation stalls
- decreased heap memory overhead requirements
- lowered garbage collection CPU overhead
Additionally, the goal is to add these advantages while preserving already existing benefits of using the non-generational approach:
- pause times lower than one millisecond
- support for heap sizes up to many terabytes
- minimal manual configuration
To maintain the last point, the new GC doesn’t need any manual configuration for the size of the generations, the number of threads used, or how long objects should reside in the young generation.
The Generational ZGC introduces a two-generation heap structure: the young generation for recent objects and the old generation for long-lived ones. Each generation is independently collected, prioritizing the frequent collection of young objects.
Concurrent collection, similar to non-generational ZGC, relies on colored pointers, load barriers, and store barriers for consistent object graph views. Colored pointers contain metadata, facilitating efficient 64-bit object pointer usage. Load barriers interpret metadata, while store barriers handle metadata addition, maintain remembered sets, and mark objects as alive.
5.4. Enabling Generational ZGC
For a smooth transition, the Generational ZGC will be available alongside the non-generational ZGC. The -XX:UseZGC command-line option will select the non-generational ZGC. To select the Generational ZGC, we need to add the -XX:+ZGenerational option:
java -XX:+UseZGC -XX:+ZGenerational ...
The Generational ZGC is intended to become the default one in a future Java release. Moreover, in an even later release, the non-generational ZGC may be removed entirely.
The integration of barriers and colored pointers in the new GC introduces higher complexity, surpassing its non-generational counterpart. The Generational ZGC also runs two garbage collectors concurrently. These collectors are not totally independent, as they interact in some cases, adding complexity to the implementation.
Although it is expected to excel in most use cases, certain workloads pose a risk of slight performance degradation. To address this issue, continuous evolution and optimization of the Generational ZGC will be driven by benchmarks and user feedback, aiming to address and mitigate these identified risks over time.
6. Generational ZGC Design Differences
The Generational ZGC introduces several design differences, enhancing garbage collection efficiency and user adaptability compared to its non-generational counterpart.
6.1. Enhanced Performance with Optimized Barriers
Generational ZGC discards multi-mapped memory in favor of explicit code within load and store barriers. To accommodate store barriers and revised load barrier responsibilities, Generational ZGC employs a highly optimized barrier code. Leveraging techniques such as fast paths and slow paths, the optimized barriers ensure maximum throughput and performance for applications, even under intensive workloads.
6.2. Efficient Inter-Generational Pointers Tracking
Double-buffered remembered sets — organized in pairs for each old-generation region — use bitmaps for efficient tracking of inter-generational pointers. This design choice facilitates concurrent work by application and garbage collection threads without the need for extra memory barriers, resulting in smoother execution.
6.3. Optimized Young Generation Collection
By analyzing the density of young generation regions, Generational ZGC selectively evacuates regions, reducing the effort required for young generation collection. This optimization contributes to quicker and more efficient garbage collection cycles for improved application responsiveness.
6.4. Flexible Handling of Large Objects
Generational ZGC introduces flexibility in handling large objects by allowing them to be allocated to the young generation. This eliminates the need for preemptive allocation to the old generation, enhancing memory efficiency. Large objects can now be collected in the young generation if short-lived or efficiently promoted to the old generation if long-lived.
As we’ve learned throughout this article, Java 21 comes with a powerful feature, the Generational ZGC. With careful consideration of potential risks and a commitment to ongoing refinement based on user feedback, it is expected to offer enhanced efficiency and responsiveness, making it a valuable addition to Java’s evolving ecosystem.