A garbage collector, or GC, as most of us know, is a background daemon task that operates inside the JVM and tries to free up memory as soon as the objects holding that memory become redundant. As advanced versions of Java are released, there has been a conscious effort to improve garbage collectors and make them attuned to the modern computing environments.
Garbage collectors are broadly classified into generational and non-generational GCs.
Generational GCs, like the ZGC, view the heap as having two generations of objects: young and old generation. They mostly work on young generation objects: objects that have just been created and will serve a temporary purpose. These objects are better collected quickly.
Non-generational GCs view the heap as a uniform collection of objects, and collect all the objects that are not needed (not referenced any more), regardless of whether they are young or old.
When Are Non-generational GCs More Effective?
Non generational GCs are more effective when your programs create a large number of long-life objects. Examples are network sockets that will remain alive until closed, connection pools with a minimum number of “always live/available” connections, data streams that keep reading newer incoming data etc.
When Are Generational GCs More Effective?
Generational garbage collection benefits programs where many short-lived objects are created and then quickly become garbage. This is because generational collectors focus on the “young generation” of objects, where most objects become eligible for garbage collection soon after being created. This approach is more efficient than a single, large garbage collection cycle for all objects.
Concurrent Compaction in Garbage Collection
Concurrent compaction in garbage collection refers to the ability of the garbage collector to compact the heap (move live objects to reduce fragmentation) while the application is running, rather than pausing it. So, we can safely assume that a concurrent, generational garbage collector that can dynamically tune the size of its young generation and related parameters can deliver low pause times while maintaining strong overall performance.
The Generational Shenandoah Garbage Collector
Java introduced the Generational Shenandoah garbage collector with Java 21 as an experimental feature. It performs garbage collection alongside other running Java threads, thereby eliminating the need to pause the Java program for GC threads to run. It’s able to do so as it focuses on collecting the younger objects.
Some key features of Generational Shenandoah:
- Generational Shenandoah works on the Weak-generational hypothesis, which posits that most objects become unreachable (not required) shortly after they are created. So, the majority of garbage collected objects are young in age: i.e. they are called young generation objects. Examples of young generation objects are function parameters, method variables, temporary variables etc. Only If an object survives long enough will it be promoted to the old generation. So, frequently scanning and garbage collecting the young generational objects is mostly CPU efficient, as these objects are collected quickly in less CPU time.
- It’s more efficient, as memory is reclaimed quickly without having to scan the whole heap. Piling up unused young generation objects to be collected with old generational objects at some later point in time will lead to longer CPU pauses, as more work will need to be done in garbage collection. So, it leads to improved performance also.
- The Generational ZGC combines the benefits of ZGC’s low pause times with the efficiency of generational collection, making it suitable for large, memory-intensive applications.
- Generational Shenandoah can only approximate but never match the efficiency tactics of stop-the-world(non-generational) GCs given its mandates to keep pause times much lower and to avoid stop-the-world compactions entirely.
To test how generational Shenandoah functions, let’s take a memory and resource-heavy Java application, run it on Java 24 and also on an older Java version, let’s say Java 17.
We chose Neoj4J, the Graph DBMS which is developed with Java and fully runs on Java.
Since it’s a DBMS, it uses a lot of memory. As we fire queries at it, many temporary or
short lived objects, like database Statement, ResultSet, Cursor etc. are created to fetch the results from the database. When the results are displayed at the Neo4J GUI or console, these objects should be garbage collected. Also, depending on the data type of the fetched column, different methods of the ResultSet object like getInt(), getString(), getDate(), getBoolean() and getDouble() would be called, leading to the creation of temporary variables which are short lived and hence qualify for young generation garbage collection.
Remember, we have said that the generational Shenandoah was developed to improve efficiency by handling young and old objects differently. It aims to identify and collect young objects quickly, thereby aiming to free up as much space as quickly as it can, without using any substantial CPU time.
Now, let’s start our experiment.
The Neo4J desktop edition comes bundled with two JVMs, the ZULU 17 and ZULU 11, which are builds based on OpenJDK so it can run on systems not having an existing JVM installed.
For our tests, we will first run Neo4J with the bundled Java 17 , and check how the GC performs. Then we will use the Java 24 installed on our local system to run the Neo4J desktop, and benchmark both. During each run we will use the yc_script from yCrash to take a 360-degree dump of all the system resources being used.
To instruct Neo4J to use my system’s local JVM, I added the following line to neo4j.conf / neo4j.cong-default / neo4j-admin.conf configuration files, in the “JVM Parameters” section.
server.jvm.additional=-Djava.home=C:/java24
The first run with inbuilt or bundled Java 17 shows the following results.
- Metric 1 – Total Memory used
Java 17

Fig: Memory Used in Java 17
Source:https://ycrash.io/yc-load-report-gc?ou=VHlhRkI1RW1rWlhoeXNsb3o4TzdaUT09&de=host&app=yc&ts=2025-06-16T12-28-17
Java 24

Fig: Memory Used in Java 24
Source:https://ycrash.io/yc-load-report-gc?ou=VHlhRkI1RW1rWlhoeXNsb3o4TzdaUT09&de=host&app=yc&ts=2025-06-16T12-21-52
It’s evident that for Java 24, which is using the Shenandoah GC, the peak memory usage for young/old/metaspace is much less. Also, The total space actually used is significantly less for Java 24.
- Metric 2 – Average GC Time
Java 17

Fig: Key Performance Indicators: Java 17
Java 24

Fig: Key Performance Indicators: Java 24
Though the difference is just a few milliseconds, still the garbage collector in Java 24 completes its work in less time.
- Metric 3 – Count of young generation objects.
Java 17

Fig: Count of Young Generation Objects: Java 17
Java 24

Fig: Count of Young Generation Objects: Java 24
No surprises here again. Since Java 24 quickly collects objects that are young, the ratio of old generation vs young generation objects is much better. There are fewer young generation objects, thereby leading to more available space in Java 24.
- Metric 4 – Count of old generation objects.
Java 17

Fig: Count of Old Generation Objects: Java 17
Java 24

Fig: Count of Old Generation Objects: Java 24
Here, though the differences are subtle, you can clearly see that for Java 24, a much lesser number of objects are promoted to old generation, which suggests that most objects are collected early, leading to quicker emptying of memory space. Also, it means that old generation objects occupy less memory in Java 24.
- Metric 5 – Allocation and Promotion Graphs
To service new incoming requests, your application will create a lot of new objects. ‘Allocation object size’ line in the graph tells you the amount of objects created by the application at a given point in time.
Objects which have survived a number of young GCs will be promoted from young generation to the old generation (where ‘n’ depends on your GC settings). ‘Promoted (Young -> Old) objects size’ line tells you the amount of objects that are promoted from the young generation to the old generation.
Java 17

Fig: Promoted Objects: Java 17
Java 24

Fig: Promoted Objects: Java 24
You can clearly see that for Java 24, far fewer objects are promoted to old generation, which means that most objects are collected early, leading to quicker emptying of memory space. Also, it means that old generation objects occupy less memory in Java 24.
- Metric 6 – The Heap usage statistics
The heap usage statistics also confirm that Java 24 uses a smaller percentage of its allocated heap memory, and uses less metaspace. Also, it uses less total space, whereas the total space used by Java17 is almost double in comparison.
Java 17

Fig: Memory Usage: Java 17
Java 24

Fig: Memory Usage: Java 24
Conclusion
So, we can safely conclude that the improved Garbage Collector of Java 24 seems to be clearing young objects quickly to make more memory available, without stopping the Java program threads. The implementation of the weak generational hypothesis seems to be working well.
Thanks for your time 🙂

Share your Thoughts!