One of the most established approaches to improving Java application performance is to tune JVM options, in particular Garbage Collector (GC) parameters.
Here is the surprising part: lowering GC overhead does not always make a Java application faster. In our benchmarks, a JVM configuration with roughly twice the GC overhead ran 29% faster than the default. The reason is that concurrent GC threads compete with application threads for CPU, and that work is never counted in the GC overhead metric. The rest of this post shows how we found this, and what it means for JVM tuning.
The main task of garbage collection is to free memory up. That requires stopping the application threads during the stop-the-world (STW) pause; application threads then resume once the garbage collection has completed. The STW interval is measured by the GC pause metric.

The percentage of time spent performing garbage collection is called GC time or GC overhead.
The conventional wisdom of GC performance is that you must always try to lower GC time as much as possible. This seems to make sense: by decreasing GC time, we get higher throughput, meaning application threads can run longer without interruption. However, when you run actual optimization studies, things can turn out very differently, as we will see.
GC time and application throughput: the golden rule of JVM tuning
The following chart, taken from the official Oracle documentation, shows the impact of GC time percentage on application throughput and scalability.

These curves basically follow the well-known Amdahl’s law, which states that the speedup of a parallel program is limited by its sequential part. In Java, the sequential part is the GC pause; the rest is parallel processing.
Looking at this chart, it is easy to see why the general goal of improving Java application throughput via JVM tuning is stated as the “golden” rule: “keep the overhead of doing garbage collection as low as possible” (as stated in the Oracle documentation).
This rule is so ingrained in our industry practices that it is not only the main best practice for JVM tuning; it is also hardcoded into many APM tools when they recommend how to tune the JVM.
However, GC overhead is far from the only factor driving application performance. More importantly, by focusing only on reducing GC overhead you may actually end up slowing your application down. A very counterintuitive fact.
Looking deeper into GC behaviour
To see how counterintuitive GC behaviour can get, we used Renaissance, a popular open-source Java benchmarking suite (renaissance.dev) that includes a Spark-based application running on OpenJDK 11. We studied how application execution time varies as the JVM and GC parameters are tuned.
To simplify this task we leveraged Akamas, our AI-powered optimization solution. Akamas launches the benchmark with different JVM configurations and automatically finds an optimal configuration driven by a custom goal, such as minimizing execution time. This let us run 80 experiments in about 20 hours, completely unattended.
The following chart shows the results. Each dot is a benchmark execution with a specific set of JVM options. The yellow dot is the JVM with default settings (the baseline configuration); the red dot is the optimal configuration.

This chart reveals a couple of interesting findings:
- The best configuration makes the application 29% faster than the baseline. That is not a small improvement, and it was achieved simply by reconfiguring the JVM.
- The best configuration also has a higher GC overhead, by almost a factor of 2x. This is very counterintuitive and runs against the golden rule.
The shape of the curve is worth a comment too. We might have expected a roughly linear relationship: the higher the GC overhead, the higher the execution time. Instead, the minimum execution times are reached at around 30% GC overhead.
It is also worth noting that the baseline sits at 15% GC overhead, which is already considered quite high. Some APM tools warn about “high overhead” at 15%. And yet the surprising result is that a configuration with even higher GC overhead (around 30%) makes the application go faster.
All of this raises the question: how can an application run faster when GC is leaving less time for the application threads? To find the answer, we had to go deeper, down to the OS level where the JVM threads are scheduled.
Behind the scenes: how concurrent GC threads compete for CPU
To understand the dynamics of JVM performance, we analyzed how application and GC threads are managed by the Linux CPU scheduler on the actual processors.
Several tracing tools can be used for this. Examples include Linux perf, the main low-level performance analysis tool for Linux, and ftrace, a facility built into the Linux kernel that traces scheduling events. We chose Perfetto, a browser-based tracing tool built by Google. It is best known among Android developers for diagnosing mobile app performance, but it also works on standard Linux distributions and provides a clear visualization.
The following chart shows the JVM threads (one per row) as they are scheduled on the CPUs. Application threads are at the top (Executor tasks); GC threads are at the bottom. The GC threads are the JVM threads that actually perform the GC work. For each thread, the bar shows when it is running: dark green when running on the CPU, light green when waiting to access the CPU.

Notice that some GC threads run concurrently with the application threads. These are the “G1 Conc” threads. This is a design feature of the OpenJDK 11 G1 Garbage Collector, which uses background threads to perform part of the garbage collection process in order to reduce the STW pause.
This is where the answer lies. The G1 GC concurrent threads compete with the application threads for CPU resources. Because these GC threads run concurrently with the application, their work is not counted in the GC overhead metric, which is exactly what breaks the golden rule.
So when you tune the JVM by following the golden rule of reducing GC overhead alone, you cannot be sure it will improve application performance. You may even cause more harm than good: you might be asking the Garbage Collector to do more concurrent work, which keeps the application threads waiting and slows the application down.
Conclusions
The key takeaway is that GC time remains an important metric to watch, but the simple golden rule of always reducing GC overhead does not always work. That rule relies on a simplistic model of what happens at the JVM level. It no longer holds in modern systems, where the JVM and the underlying system interact in complex, sometimes unpredictable ways that have real effects on end-to-end application performance.
This is just one instance of a broader situation in performance tuning today. The complexity of applications (monolithic or microservice-based) and infrastructures (on-premise, cloud, or hybrid) makes it very hard to tune by relying on predefined rules and vendor best practices.
Akamas gives performance engineers the ability to optimize application performance by intelligently exploring thousands of configurations, without relying on predefined rules or human knowledge. Its specialized AI techniques find the optimal configuration for your custom goals (for example, performance and cost tradeoffs) and constraints (for example, SLOs) in just a few hours.
If you want to go further on JVM tuning, see our guide on how to choose the right Java garbage collector, how Akamas handles application runtimes such as the JVM and Node.js, and, for Kubernetes, JVM on Kubernetes optimization and the State of Java on Kubernetes 2026.
In the following blog entries we will continue to debunk other JVM performance tuning myths and advocate for a new approach to performance optimization. Stay tuned!
FAQs
What is GC overhead?
GC overhead, also called GC time, is the percentage of time an application spends performing garbage collection instead of running application code. It is a standard JVM performance metric. As this article shows, it does not capture concurrent GC work, so it is an incomplete picture of GC cost.
Is high GC overhead always bad?
No. High GC overhead is often treated as a problem, and some APM tools flag it at 15%. But in our benchmarks a configuration at around 30% GC overhead ran 29% faster than the baseline. GC overhead should be read alongside end-to-end application performance, not on its own.
How is GC overhead different from the “GC overhead limit exceeded” error?
They are different things. GC overhead is a performance metric (a percentage of time). “GC overhead limit exceeded” is a specific OutOfMemoryError the JVM throws when the collector runs almost constantly while reclaiming very little memory. The metric describes cost; the error signals a memory problem.
Which garbage collector and JDK did the study use?
The study used the G1 Garbage Collector on OpenJDK 11, benchmarked with the Renaissance suite. The counterintuitive result comes largely from G1’s concurrent (“G1 Conc”) threads, whose CPU work is not counted in GC overhead.

