This article kicks off a series of posts related to the release of the new Java 25. We will focus on the changes that can impact application performance. In this post, we will focus on the Shenandoah garbage collector (GC) which now officially supports a generational algorithm.
What is the Shenandoah GC ?
The Shenandoah GC is one of the garbage collectors provided on the HotSpot JVM that can be enabled to manage the memory of the JVM. The following table lists the existing garbage collectors and their main usage for Hotspot:
| Garbage Collector | Usage |
|---|---|
| Serial GC | Single-threaded collector, best suited for single-core machines or small applications |
| Parallel GC | Multi-threaded collector focused on maximizing throughput |
| Concurrent Mark-Sweep 🕱 | Removed in JDK14 |
| G1 GC | Designed to balance latency and throughput. Default GC |
| ZGC | Low-latency scalable collector with sub-millisecond pause times |
| Shenandoah | Low-pause-time collector like ZGC, also concurrent compaction. |
| No-op GC | Test only. Allocates memory but never reclaims it. Useful for testing and performance evaluation without GC overhead |
The Shenandoah GC was introduced in 2019 with Java 12, and aims at providing a guaranteed low pause time for latency-sensitive applications. What you can expect with this GC is a pause time in the 1-10 milliseconds range, independently of the heap size.
To achieve this low level of pauses, the GC is almost fully concurrent. The memory is divided into regions, and the GC works to mark, evacuate and compact objects in some of the regions while the application is running. There is a small “stop the world” pause at the end of a collection cycle, but it is brief as most of the tasks were done while the application is running.
Let’s see how it compares with the other GC:
G1GC is the default garbage collector since JDK 9 because it is the most balanced garbage collector between throughput, memory usage and latency. G1GC is also a region-based garbage collector, but it only performs the marking phase while the application is running and needs to pause the application to perform the compaction phase. That explains why Shenandoah can provide lower latencies than G1GC (~200ms). The tradeoff is that Shenandoah requires more memory (10-20% overhead on the heap) compared to G1GC (<10%)
ZGC is a very interesting GC to compare with Shenandoah as they are both designed to be low-pause garbage collectors. So why do we have both ? Because they have been concurrently developed by different actors. ZGC has been developed by Oracle and released since JDK 11. Shenandoah has been developed by Red Hat and released since JDK 12. The Oracle JDK does not include Shenandoah in an effort to promote their own ZGC.
Even if they are both low-pause GC, Shenandoah has been designed for medium-sized heaps (8 to 64GB) while ZGC is designed for extra large heaps (hundreds of GBs and beyond).
What is the generational Shenandoah?
Generational GCs were developed based on the observation that most “objects die young” in the heap, and if these objects are not dead after a few collection cycles, there is no point checking them at every single collection because they will most likely live very long.
To reduce the collection cycle, Generational GCs split the heap into a space for recent objects (young) and a space for objects that survived a few cycles (old). The collection strategies differ for these different generations, which removes unnecessary scans and improves performance.
All mature GCs are generational, but the generational feature is often added after the initial release of the GC. ZGC was released in 2018 and added the generational feature in JDK 23. Remember that ZGC and Shenandoah are competitors and follow the same trajectory, so it was not a surprise to see the Generational Shenandoah released in JDK 24 as an experimental feature.
The generational Shenandoah should mostly improve the throughput and the memory footprint compared to non-generational Shenandoah. I have heard the number of ~30% higher throughput, but did not find any precise benchmark to corroborate this number.
What has changed for Shenandoah GC in Java 25 ?
In Java 24, the Shenandoah GC generational algorithm was released as an experimental feature, so that its correctness can be proven. You can use it in Java 24 using the following flags:
-XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational
-XX:+UnlockExperimentalVMOptions
Between Java 24 and 25, the work continued on this GC to:
- Stabilize the implementation and fix bugs and security issues
- Improve performance by reducing scanning, GC overhead and reduce the variability of pauses
- Optimize region handling to avoid promoting short-lived objects
- Improve tooling and logging
Thanks to all these improvements, the Generational Shenandoah has now reached a level of maturity that makes it suitable for production-grade applications, and has been made an official feature. That means that if you were using it in Java 24, you do not need the experimental flag anymore, and if you were using the Shenandoah GC without the generational feature, it may be worth considering it.

Top comments (0)