Java Garbage Collection
Learn everything you need to know about the newest garbage collectors in the JVM.
Join the DZone community and get the full member experience.Join For Free
Java manages heap memory for you. You run your application inside a virtual machine (JVM). The JVM does the work of allocating space when necessary and freeing it when it is no longer needed. The garbage collector (GC) is what does this work for you.
Recycling memory involves garbage collection "cycles" that have an impact on performance. How much impact depends on the nature of your application and the GC you choose. The good news is that there's more than one garbage collector, and each recent release ofJava has offered more choices. You can pick the GC that best suits your application's needs.
New Garbage Collectors
The last two releases of Java introduced three new GCs. We'll take a look at each of them and what they add to the ecosystem.
Java 11 introduced Epsilon and the Z garbage collector (ZGC). Epsilon is the "no-op" garbage collector. It allocates new memory but never recycles it. ZGC promises to manage vast amounts of memory with high throughput and short pause times.
Java 12 adds the Shenandoah Garbage Collector. Like ZGC, it manages large heaps with short pause times but uses a very different approach.
Epsilon Garbage Collector
What Is Epsilon?
Epsilon is a passive or "no-op" GC. It handles memory allocation but doesn't recycle it when objects are no longer used. When your application exhausts the Java heap, the JVM shuts down. In other words, Epsilon will allow your application to run out of memory and crash.
How to Use Epsilon
|Command Line Options
Unlock Java experimental options.
Use Epsilon GC
Set heap size. JVM will exit with an OutOfMemoryError if this amount is exceeded.
Generate a heap dump if JVM runs out of memory.
Run specified command when an out-of-memory error occurs.
Why Use a No-Op GC?
A no-op garbage collector is useful for measuring and managing application performance. Active garbage collectors are complex programs that run inside the JVM alongside your application. They incur overhead that can cause latency and reduce throughput.
Epsilon removes the impact GC has on performance. There are no GC cycles or read or write barriers. When using the Epsilon GC, your code runs in isolation. You can use it to see how garbage collection affects your app's performance and what your memory threshold is since it'll tell you when it runs out. If you think you only need four gigabytes of memory, run it with
-Xmx4g and see if you're right. If you're wrong, rerun it with
XX:HeapDumpOnOutOfMemoryError enabled and take a look at the heap dump to see where you're wrong.
If you need to squeeze every bit of performance out of your application, Epsilon might be your best option for a GC. But you need to have a complete understanding of how your code uses memory. If it creates almost no garbage or you know exactly how much memory it uses for the period it runs in, Epsilon is a viable option.
The Z Garbage Collector
What Is the Z Garbage Collector?
ZGC is a low-latency GC designed to work well with huge amounts of memory. The Oracle documentation refers to multi-terabyte heaps in its description of Z. Oracle introduced ZGC in Java 11. In Java 12, Oracle added performance fixes and class unloading even though Z is still in experimental status. It's only available on 64-bit Linux.
How Does ZGC Work?
ZGC works concurrently with your application, performing all its work in its threads. It uses load barriers for heap references. Load barriers cause fewer delays than those imposed by the G1 collector's pre- and post-write barriers.
ZGC takes advantage of 64-bit pointers with a technique called pointer coloring. Colored pointers store extra information about objects on the heap. (This is one of the reasons it's limited to the 64-bit JVM.) By limiting the GC to 4TB heaps, the developers have 22 extra bits in each pointer to encode additional information. Z uses four extra bits at the moment. Each pointer has a bit for finalizable, remapped, mark0, or mark1.
The Z garbage collector remaps objects when the memory becomes fragmented. The mapping avoids the performance hit incurred when the GC needs to find space for a new allocation. Pointer coloring helps with remapping since a remapped reference discovers the new location at the next access.
When your application loads a reference from the heap, ZGC checks the extra bits. If it needs to do any extra work (e.g. getting a remapped instance), it handles it in the load barrier. It only has to do this once, when it loads the reference. This sets it apart from the write barriers used by mainline garbage collectors like G1.
The Z garbage collector performs its cycles in its threads. It pauses the application for an average of 1 ms. The G1 and Parallel collectors average roughly 200 ms.
How to Use ZGC
|Command Line Options
Unlock Java experimental options
Set heap size.
Set number of GC threads
ZGC is a concurrent garbage collector, so setting the right heap size is very important. The heap must be large enough to accommodate your application but also needs extra headroom so Z can meet new requests while relocating active objects. The amount of headroom you need depends on how quickly your application requests new memory.
ZGC will try to set the number of threads itself, and it's usually right. But if ZGC has too many threads, it will starve your application. If it doesn't have enough, you'll create garbage faster than the GC can collect it.
Why Use ZGC?
ZGC's design works well with applications large heap sizes. It manages these heaps with pause times under 10ms and little impact on throughput. These times are better than G1's.
ZGC does its marking in three phases. The first is a short stop-the-world phase. It examines the GC roots, local variables that point to the rest of the heap. The total number of these roots is usually minimal and doesn't scale with the size of the load, so ZGC's pauses are very short and don't increase as your heap grows.
Once the initial phase completes, ZGC continues with a concurrent phase. It walks the object graph and examines the colored pointers, marking accessible objects. The load barrier prevents contention between the GC phase and any application's activity.
After ZGC has completed marking, it moves live objects to free up large sections of the heap to make allocations faster. When the relocation phase begins, ZGC divides the heap into pages and works on one page at a time. Once ZGC finishes moving any roots, the rest of the relocation happens in a concurrent phase.
ZGC's phases illustrate how it manages large heaps without impacting performance as application memory grows.
What Is Shenandoah?
Shenandoah is another garbage collector with low pause times. These times are short and predictable, regardless of the size of the heap. Shenandoah was developed at Red Hat and has been around for several years. It's now part of the Java 12 release.
Like ZGC, Shenandoah does most of its work in parallel with the running application. But its approach to garbage collection is different. Shenandoah uses memory regions to manage which objects are no longer in use and which are live and ready for compression. Shenandoah also adds a forwarding pointer to every heap object and uses it to control access to the object.
How Does Shenandoah Work?
Shenandoah's design trades concurrent CPU cycles and space for pause time improvements. The forwarding pointer makes it easy to move objects, but the aggressive moves mean Shenandoah uses more memory and requires more parallel work than other GCs. But it does the extra work with very brief stop-the-world pauses.
Shenandoah processes the heap in many small phases, most of which are concurrent with the application. This design makes it possible for the GC to manage a large heap efficiently.
The first phase contains the first stop-the-world pause in the cycle. It prepares the heap for concurrent marking and scans the root set. LikeZGC, the length of this pause corresponds to the size of the root set, not the heap. Next, a concurrent phase walks the heap and identifies reachable and unreachable objects.
The third finishes the process of marking by draining pending heap updates and re-scanning the root set. This phase triggers the second stop-the-world pause in the cycle. The number of pending updates and size of the root set determine how long the pause is.
Then, another concurrent phase copies the objects out of the regions identified in the final mark phase. This process sets Shenandoah apart from other GCs since it aggressively compacts the heap in parallel with application threads.
The next phase triggers the third (and shortest) pause in the cycle. It ensures that all GC threads have finished evacuation. When it finishes, a concurrent phase walks the heap and updates references to objects moved earlier in the cycle.
The last stop-the-world pause in the cycle finishes updating the references by updating the root set. At the same time, it recycles the evacuated regions. Finally, the last phase reclaims the evacuated regions, which now have no references in them.
How to Use Shenandoah
|Command Line Options
Unlock Java experimental options.
Use Shenandoah GC.
Set heap size.
You can configure Shenandoah with one of three heuristics. They govern when the GC starts its cycles and how it selects regions for evacuation.
1. Adaptive: Observes GC cycles and starts the next cycle so it completes before the application exhausts the heap. This heuristic is the default mode.
2. Static: Starts a GC cycle based on heap occupancy and allocation pressure.
3. Compact: Runs GC cycles continuously. Shenandoah starts a new cycle as soon as the previous finishes or based on the amount of heap allocated since the last cycle. This heuristic incurs throughput overhead but provides the best space reclamation.
Shenandoah needs to collect heap faster than the application it's serving allocates it. If the allocation pressure is too high and there's not enough space for new allocations, there will be a failure. Shenandoah has configurable mechanisms for this situation.
- Pacing: If Shenandoah starts to fall behind the rate of allocation, it will stall allocation threads to catch up. The stalls are usually enough for mild allocation spikes. Shenandoahintroduces delays of 10ms or less. If pacing fails, Shenandoah will move to the next step: degenerated GC.
- Degenerated GC: If an allocation failure occurs, Shenandoahstarts a stop-the-world phase. It uses the phase to complete the current GC cycle. Since a stop-the-world doesn't contend with the application for resources, the cycle should finish quickly and clear the allocation shortfall. Often, a degenerated cycle happens after most of the cycle's work is already completed, so the stop-the-world is brief. The GC log will report it as a full pause, though.
- Full GC: If both pacing and a degenerated GC fail, Shenandoah falls back to a full GC cycle. This final GC guarantees the application won't fail with an out-of-memory error unless there's no heap left.
Why Use Shenandoah?
Shenandoah offers the same advantages as ZGC with large heaps but more tuning options. Depending on the nature of your application, the different heuristics may be a good fit. Its pause times might not be as brief as ZGC's, but they're more predictable.
While Shenandoah was not made available as part of Java until version12, it's been around longer than ZGC. It's seen more testing and is even available as a backport for both Java 8 and 10.
Java GCs have come a long way in a few short years. The default G1collector has made significant improvements since Java 7, and three new ones have been added to the platform.
Depending on your needs, one of these new GCs may be what you need to take your Java application performance to the next level.
Opinions expressed by DZone contributors are their own.