The Java platform keeps getting better. The OpenJDK community has been busy improving the performance of the JVM and the garbage collector (GC): new collectors are being developed, and existing ones are constantly refined to make Java applications run faster and more efficiently.
So when you want better application performance, it is fair to ask whether you should still tune the JVM at all. Why not simply move to a newer JVM, take the improved out-of-the-box behavior, and rely on the new low-latency garbage collectors? In other words, should you just leave application performance and efficiency to the JVM?
There is no single best Java garbage collector. The right choice depends on your goal: throughput, latency, or memory footprint. In Akamas benchmarks, switching from the default G1 collector to Parallel GC made one Java application 22% faster while using 30% less memory. GC selection and JVM tuning still deliver large gains, even on modern JVMs. If you care about performance, cost, and resiliency, it pays not to “just leave it to the JVM.”
The rest of this post shows why, using two studies from our labs. In each one we run extensive performance testing and benchmarking to understand how JVMs actually behave, and how best to optimize them.
Insights on G1 garbage collector heuristics
G1 has been the default garbage collector since OpenJDK 9, and it remains the default through the current LTS releases (JDK 17 and JDK 21). It is a significantly more complex collector than Parallel: it relies on many internal heuristics that decide when to collect garbage, how much to collect, and how much memory to allocate. All of these decisions ultimately shape application performance and efficiency.
Our first study set out to understand how G1 GC behaves, in terms of application performance and resource footprint, under varying heap sizes. For the setup we used Renaissance, a popular open-source Java benchmark.
We ran several experiments, increasing the Java max heap from 2 GB to 5 GB, with all other JVM options left at their defaults. During each run we measured a few key metrics: application execution time, max heap memory used, and GC metrics such as GC time (the overhead), GC CPU time, and pauses from the GC log files.

The results hold a few surprises. As you would expect, execution time falls as more memory is allocated. But the GC overhead percentage rises with larger heaps, which is the opposite of what most people expect. (For more on why JVM metrics can be poor predictors of real application performance, see our companion post on why lowering GC time may not lead to better performance.).
One G1 behavior stands out. Look at the run with a 5 GB heap, highlighted in the next figure.

Here the GC overhead is close to 20%, already significant: application threads are halted 20% of the time. Yet G1 chose to use only 3.4 GB of the 5 GB available. It did not inflate the heap, even with 1.6 GB of headroom on the table. G1 also burned a lot of CPU: 172 seconds of CPU time went to garbage collection. Since the run took 223 seconds, that is almost 80% of one CPU core spent on GC work.
So what happened? G1 traded CPU and application throughput for memory efficiency. This is a common G1 behavior we have seen many times in production Java applications. That is fine if memory is your constraint. But what if your goal was lower CPU usage, higher efficiency, and lower cloud cost? Or higher application throughput? G1 was simply optimizing for different goals than yours.
Why does this happen? The JVM, like any runtime, OS, or database, has to make complex decisions about performance and resource management. It makes them with built-in heuristics: rules and thresholds implemented around the collector’s own design goals. Those heuristics can behave very differently from your end-to-end performance or efficiency targets. Worse, they are driven by internal JVM metrics, not by real application performance indicators.
What is the best JVM garbage collector?
Our second study looked at how much the choice of collector alone affects application performance and resource footprint.
We reused the setup from the previous experiments, with one change: we held the heap at 8 GB throughout. This time we ran OpenJDK 15, so we could assess the newer low-latency collectors. We tested five garbage collectors:
- Serial: the single-threaded collector; simple and efficient for a small memory footprint.
- Parallel: the multi-threaded collector; ideal for throughput-oriented applications.
- G1: the multi-threaded collector; designed to balance latency and throughput.
- Z and Shenandoah: the low-latency collectors available from OpenJDK 15.
We used the Akamas platform to automate the benchmarks, gather the metrics, and compute the performance deltas. Akamas also kept results repeatable and stable: it excluded warmups automatically and ran multiple trials to confirm low variability.

The results are striking:
- Parallel wins for this application. It is about 22% faster than the default G1 configuration. It is also more memory efficient, using 30% less memory.
- Serial performs surprisingly well. It runs about 10% slower, but it is very efficient on resources: 22% less CPU and nearly half the memory of the default G1. That is notable at an 8 GB heap, where a single-threaded collector might be expected to struggle.
- Z and Shenandoah are both slower and less memory efficient here. Z in particular used 67% more memory than G1. Their OS load average was also much higher, most likely from the extra concurrent work these collectors perform.
These results are specific to this application. But we have seen similar patterns across many customer production applications, which confirms a simple point: the default JVM configuration and collector can be far from optimal for a given application and its performance requirements. Collectors keep improving, yet the need to tune the JVM and select the right GC will remain. As Aleksey Shipilev, developer of the Shenandoah GC and a leading JVM performance expert, put it in his Devoxx Belgium 2017 talk:
Universal GC does not exist. You have either low latency, or high throughput or low memory footprint. For your application you have to decide which GC you want to use.”
Aleksey Shipilev – Principal Software Engineer, RedHat
Conclusions
We asked whether JVM tuning still has a place today, when new JVMs ship with ever-greater performance improvements.
The answer is yes. As we showed, choosing the best collector for your application can change performance and efficiency dramatically. Despite real improvements, the JVM remains a complex system whose default behavior does not necessarily match your specific requirements. Leave performance, efficiency, and reliability entirely to the JVM, and you are likely leaving significant gains on the table.
There is a lot of room to improve the performance, resilience, and cost of your applications by finding the JVM parameters that are optimal for each application and workload. In a world of complex architectures, each built on many technologies with dozens or hundreds of parameters (whose behavior sometimes changes from one version to the next), that is a daunting task. It cannot be done well with predefined rules and vendor best practices alone.
Akamas gives performance engineers a way to optimize application performance by exploring thousands of configurations automatically, without relying on predefined rules or human guesswork. Its specialized AI finds the configuration that best fits your goals (for example, a performance and cost tradeoff) and your constraints (for example, SLOs), in just a few hours. To see how this applies on Kubernetes, where the JVM and the container fight over the same memory, read preventing OOMKilled with JVM-aware optimization.
In the next posts we will keep debunking JVM performance tuning myths and making the case for a new approach to optimization. Stay tuned.
FAQs
What is the best Java garbage collector?
There is no universal best Java garbage collector. The right choice depends on your priority: throughput, latency, or memory footprint. In Akamas benchmarks, Parallel GC was 22% faster than G1 for a throughput-oriented workload, while Serial GC nearly halved memory use. Test the collectors against your own goals.
What is the default garbage collector in Java?
G1 is the default garbage collector from OpenJDK 9 onward, including the current LTS releases (JDK 17 and JDK 21). G1 balances latency and throughput, but its defaults are tuned for general use, not for your specific application or SLOs.
Is Parallel GC faster than G1 GC?
It can be, depending on the workload. In one Akamas benchmark on OpenJDK 15, Parallel GC ran about 22% faster than the default G1 and used 30% less memory. Parallel favors raw throughput; G1 favors predictable pauses. The winner depends on your goal.
Should I still tune the JVM, or just rely on a newer version?
You should still tune it. Newer JVMs improve default behavior, but defaults are generic. Akamas benchmarks and customer results show large gains from selecting the right collector and tuning JVM parameters per application, even on modern releases.
How does the Java garbage collector work?
The JVM garbage collector reclaims memory used by objects your application no longer references. Each collector uses different heuristics to decide when to collect, how much to collect, and how much memory to hold. Those heuristics are driven by internal JVM metrics, which is why they can diverge from your real performance goals.

