Efficient garbage collection (GC) is essential for performance-critical tasks. If you have the right settings, GC works quietly in the background, always making sure there is enough memory for incoming requests.

Get it wrong, and you have application stalls, poor response times, dropped sessions, and unhappy customers. 

Modern JVMs offer a wide choice of GC algorithms, each with its own strengths and weaknesses. How do you choose which one’s best for your application? Can AI help you make the right choice?

In this article, we’ll look at how to select the best algorithm tailored to individual situations, and how AI can streamline the decision process.

Which Java GC Algorithm Is Best for Your Application?

If you’re not familiar with how Java garbage collectors work, it’s worth reading this article to get some background: What is Garbage Collection?

Most JVM platforms include seven different garbage collection algorithms. We can select the one we want using command-line arguments.

The table below is a summary of the GC algorithms and when we might use them:

GC AlgorithmUse cases
Serial GCVery early JVM versions, tiny heap or single-core platforms
Parallel GCBatch processes; high throughput, latency-tolerant applications
CMS GCDeprecated; removed from Java 14.
G1 GCRecommended if heap size is <32GB; good balance between throughput and latency.
Shenandoah GCRecommended for large heap (>32GB) on multi-core systems. OpenJDK answer to ZGC
ZGCRecommended for large heap (>32GB), late version of JVM ( > Java 20). Allocation stalls may occur in earlier JVM versions
Epsilon GCNo-op GC used only for benchmarking in the lab

When we’re deciding which GC algorithm to use for an application, this flowchart is a good starting point.

Fig: Choosing the Right GC Algorithm for Your Application

For more information on choosing the right algorithm for your situation, see Best Java GC Algorithm

Is selecting an algorithm really that simple? 

No, it’s not. Every workload is different; every environment is different. What’s recommended in theory is not always the best solution in practice.

So, how should we go about making our choice? The answer begins long before an application is released into production. We should always take time to evaluate GC algorithm efficiency during performance lab testing. Here are some suggestions:

  • Have clear performance goals in mind before you start.
  • There is always a trade-off in a real-life situation, and you need to decide which performance goal is most important to you. For your application, list the following in order of your priority:
    • Throughput: How much time the application spends doing actual work, rather than collecting garbage;
    • Latency: How long application threads are paused while GC carries out critical tasks. This can be the most important metric in real-time applications, where even a fraction of a second’s delay can break the system.
    • Footprint: the resources used by the GC.
  • Always enable GC logging in performance labs and in production. It has very little overhead, and the logs contain valuable diagnostic data.
  • Monitor the logs using a GC Analyzer tool such as GCeasy or GCViewer, paying attention to key metrics such as latency, throughput, and footprint.
  • Initially set the GC algorithm according to the above flowchart.
  • Choose a period that includes both peak and off-peak times, analyze the logs, and record key metrics.
  • Repeat this test using two or three other algorithms that may be suitable. Record the metrics, then compare the results. This should give you the facts you need to make an informed choice.
  • Tune the GC algorithms to best advantage before rerunning the tests.

However, this can be time-consuming and tedious, with a lot of manual tasks, such as recording, comparing, and calculating the trade-off. 

Can’t we just ask our favorite LLM to recommend the best algorithm? Let’s see.

Can AI Select the Best Java Garbage Collector?

AI is an amazing tool, but it can only ever be as good as the information it’s able to access. If you were to ask even the best of the AI models, “Which GC algorithm is best for my application?”, chances are it would ask you about your heap size, your performance goals, and the platform you’re working on. It would use its best-practice information to apply logic very similar to the flowchart above before arriving at an answer. 

This answer stands a very good chance of being right: the logic works for most scenarios. It may not necessarily be the best solution for your application. There are too many factors that influence how well a particular GC works. What’s the allocation rate? How long-lived are most of the objects? How big are the largest objects? How heavy is the peak load? How many CPU cores are actually available to the application, allowing for other tasks running on the same machine? Do other tasks share the same peak times? The list of questions is almost endless, and there’s no fixed logic to deal with all of them.

This is where deterministic AI comes into its own. This almost sounds like a contradiction in terms. Deterministic problem-solving applies a fixed set of rules, and the same underlying data always produces the same result. AI, on the other hand, works on the highest probability based on whatever information it has at a given moment. If you ask it a question today, then ask it the same question tomorrow, the results may be different.

Probability is not good enough for critical production systems: we need decisions based on facts. Deterministic AI combines the best of both worlds. It uses real facts as a basis, then applies the rules to produce accurate summarized information. This can then be fed into AI as a basis for accurate dialog between the technician and the LLM. This approach combines the versatility of AI with genuine, fact-based data. 

Fig: GCeasy’s Deterministic AI Workflow

The deterministic parser aggregates, interprets and summarizes the facts. Using our chosen LLM, we can then automate further tasks, such as comparing data, and presenting facts as easy-to-understand tables, charts and projections.

See this article for more information on GCeasy’s Deterministic AI.

Let’s demonstrate deterministic AI in action to see how it works.

Case Study: Using GCeasy’s Deterministic AI to Select the Best GC Algorithm

For this case study, we picked a very small Jenkins system running on an old Ubuntu machine with very limited CPU cores and RAM. It’s using OpenJDK 21. According to best industry advice, G1GC should be the right algorithm for this situation.

But is it? We used deterministic AI to find out.

Keeping the workload, the heap size and the duration the same for each test, we enabled GC logging to record performance while testing with five different GC algorithms: Serial, Parallel, G1GC, ZGC and Shenandoah.  We then fed these logs into GCeasy’s Deterministic AI, linked to Claude as the LLM.

For this exercise, we ran with each algorithm for approximately an hour. In real life, we would have picked a much longer period for more accurate results.

Let’s see what happened.

1. Defining the Task and Loading the First Log 

When we logged into GCeasy, it gave us the choice of using the classic tool, or using Deterministic AI. We chose Deterministic AI, which then showed a screen allowing us to upload a log and ask a question. Since AI works best if we carefully explain what we’re doing and what we would like to achieve, we asked:

In this exercise, I would like to load several GC logs for the same application run using different GC algorithms. At the end of the exercise, I would like to have a table showing different algorithms going across, with rows of the following metrics for each: 

GC Throughput, Avg. Pause Time, Max Pause Time, Object Creation Rate, Object Promotion Rate, Peak Heap Size, CPU Usage.

After each log load, can you display the table so far. When I indicate that I’m loading the last log, can you show the table, and be ready to answer further questions against this data.

Here is the first log.”

See the screenshot below.

Fig: GCeasy Deterministic AI: Opening Screen

The tool responded as follows:

Fig: AI Response to First Log: Part 1

GCeasy parsed the log and submitted the analysis to Claude. Claude acknowledged the task and displayed metrics from the first log.

Fig: AI Response to First Log, Part 2

2. Uploading Subsequent Logs

We then loaded three more logs, requesting interim results:

Fig: Loading Subsequent Logs

The LLM responded to each:

Fig: Response to Subsequent Logs

The answer included the requested metrics.

3. Loading the Last Log and Requesting Results

When we loaded the last log, we advised the LLM that this was the last log, and requested a table of results:

Fig: Loading the Last Log

The LLM responded with a summary of patterns and trends, along with recommendations.

Fig: Response to Final Log: Patterns and Trends

4. Comparing the Results Across Algorithms

Next, we want to see a table comparing the metrics across all five GC algorithms. Initially, the table wasn’t suitable for copying to a spreadsheet, so we requested it in CSV format.

Fig: Table of Results in CSV Format

When we copied it to Excel, the data looked like this:

MetricSerialParallelG1GCZGCShenandoah
Heap Size (Total Allocated)247 mb247 mb256 mb256 mb256 mb
Heap Peak Usage208 mb147 mb255 mb216 mb231 mb
Young Gen Allocated76.81 mb76 mb171 mbn/an/a
Old Gen Allocated170.69 mb171 mb85 mbn/an/a
Metaspace Allocated101.19 mb89.19 mb105.62 mb85 mbn/a
Total Data Created2.49 gb1.71 gb1.66 gb1.72 gb1.11 gb
Measurement Duration1 hr 11 min 24 sec1 hr 1 min 23 sec59 min 37 sec56 min 19 sec58 min 54 sec
Avg Allocation Rate609 kb/sec487 kb/sec485 kb/sec533 kb/sec328 kb/sec
Minor GC Count37374700
Full GC Count10100
Minor GC Avg Pause24.2 ms9.15 ms67.9 msn/an/a
Minor GC Max Pause60.0 ms20.0 ms471 msn/an/a
Max Pause Time (Overall)180 ms20.0 ms142.455 ms0.055 ms1.056 ms
Avg Pause Time (Overall)28.34 ms9.15 ms17.59 ms0.016 ms0.175 ms
Throughput99.98%99.99%99.97%100.00%100.00%
CPU Time1 sec 500 ms770 ms2 sec 210 msn/an/a
Problems DetectedNoneNoneNoneNoneNone

All five algorithms performed adequately, other than Serial having a high maximum pause time. 

5. Creating Comparison Graphs from the Results

To visualize these results, we requested Claude to show four key metrics in bar chart form.

Now we have something we can really work with. All algorithms performed very well on throughput, with ZGC and Shenandoah winning by a short head. Surprisingly, G1GC performed below Parallel, ZGC, and Shenandoah on both throughput and latency. If we had gone on probability alone, G1GC would have been recommended as the best option. 

Surprisingly, Parallel, ZGC, and Shenandoah all outperformed G1GC for this particular workload on this specific hardware. G1GC would normally be the best collector for a small Jenkins server, but it’s not showing to advantage here. 

6. Requesting Tuning Suggestions

Since G1GC’s results were disappointing, we asked the LLM to suggest whether G1GC would benefit from a change in the tuning parameters. We used jcmd <PID> VM.flags  to retrieve the existing JVM options.

The LLM responded with a performance overview, followed by various tuning suggestions.

Fig: Request for Tuning Suggestions

Working from its suggestions, we added or amended the following tuning parameters on Jenkins and reran the G1GC test:

  • -Xms256m -Xmx256m 
  • -XX:+HeapDumpOnOutOfMemoryError 
  • -XX:HeapDumpPath=/home/jill/shared 
  • -XX:MaxMetaspaceSize=200m 
  • -XX:MetaspaceSize=150m
  • -XX:G1HeapRegionSize=2m
  • -XX:MaxGCPauseMillis=50
  • -XX:G1HeapRegionSize=2m
  • -XX:G1ReservePercent=15
  • -XX:ConcGCThreads=2
  • -XX:+UseStringDeduplication

7. ZGC vs G1GC vs Parallel GC: Summary of Final Results

After the rerun of the G1GC test, we used the LLM to compare the key performance indicators.

Since Serial GC was the outlier in the original test, we omitted it from the final results. We also omitted Shenandoah to give us an easier comparison, since its results were so similar to ZGC. The table below compares throughput, average and maximum pause times for each of these tests.

MetricParallelG1GC (Before)G1GC (After)ZGC
Throughput %99.99199.97299.99100
Avg Pause Time9.15 ms17.59 ms17.16 ms0.016 ms
Max Pause Time20.0 ms142.455 ms40.0 ms0.055 ms

Throughput on all tests is excellent. G1GC still has much longer pause times than the other algorithms, even after tuning.

In a real-life situation, we may still look further into G1GC tuning to get better results. For example, we could reduce -XX:MaxGCPauseMillis=50 to a number comparable to the other test results, and see whether this would adversely affect the throughput. It’s very likely that ZGC would still outperform G1GC in this situation. Our next step would be to implement ZGC, but monitor key performance indicators regularly to make sure it’s still performing to the same standard over a longer period. If not, we would relook at GC tuning and algorithm selection.

It’s beyond the scope of this article to investigate why G1GC’s performance did not compare favorably to ZGC in this test, since we’re more interested in seeing how best deterministic AI can streamline the benchmarking process.

Possibly G1GC would have benefitted from a longer test, since it self-tunes adaptively over time. It may also be because that particular workload favored ZGC’s highly-concurrent approach.

These results apply to this particular workload and environment only. Critical applications need to be tested in the same way in their own environment, and the results will obviously be different. 

Conclusion

When best performance is a must-have, it’s essential to implement and tune the right garbage collector. Nothing kills performance faster than an inefficient GC algorithm

We can use a formulaic approach to estimate which GC is best for our application, but this may not be accurate in every situation. Far too many factors affect GC behavior to be able to thumb-suck the right solution.

AI on its own can come up with an answer that works most of the time, but for critical applications, we need to test different garbage collectors to see which performs best. Evaluating the tests is time-consuming, and this is where deterministic AI shows its advantage. 

GCeasy accurately parses the logs, summarizing them into reliable metrics, which are then passed to an LLM. We can use the power of AI  to compare the results, present them in a way that’s easily understood, and interactively explore solutions.