Skip to content

JVM default ergonomics

Posted on:July 24, 2026

This post is a bonus section of the series “Learning JVM like a madman”. Today we will explore JVM default ergonomics; we will go through the definition and test it with some example code. Grab a coffee and have fun ☺️.

One note: today’s topic doesn’t cover GC tuning, because there is so much that could be presented. Going into the detail of GC tuning — young GC, old GC, new heap ratio, and so on — each of those things has its own 10+ flags to tune. So it’s not my job today, but in the near future 😛

I. JVM default ergonomics

Most of us assume that if we don’t configure any JVM options, then the JVM simply runs with “default settings.” While this is technically true, it is easy to misunderstand what “default” really means. The JVM is not running without optimization. Instead, the JVM is already making performance optimization decisions on your behalf. It automatically chooses:

These automatic decisions are collectively called JVM ergonomics.

JVM default ergonomics is the built-in feature where the Java Virtual Machine automatically selects the garbage collector, compiler type, and heap size based on your computer’s hardware to boost performance

Ergonomics Are Already an Optimization

A common software engineering principle you may know is: make it run first, don’t optimize prematurely. Interestingly, the JVM itself does not follow this advice 😉.

The moment a Java application starts, the JVM immediately begins optimizing its execution by selecting configuration values that it believes are appropriate for the current machine and workload. Even if you never specify a single JVM option, you are still accepting an optimization strategy—just one chosen by the JVM instead of yourself. Doing nothing is not the absence of optimization. It simply means:

“I trust the JVM’s optimization decisions.”

What Is the Goal of JVM Ergonomics?

The primary goal of JVM ergonomics is not to maximize performance. Instead, its goal is to make almost every Java application run reasonably well without requiring manual tuning. The JVM attempts to balance several competing objectives:

In other words, ergonomics tries to find a configuration that works well for the majority of applications on the majority of machines. Its philosophy is:

“The application should start successfully and perform reasonably well.”

This is very different from trying to achieve the absolute best performance. So from the JVM’s perspective:

However, production systems often have very different goals. As backend engineers, we usually care about:

These goals are far more aggressive than the JVM’s generic defaults.

Ergonomics Are a Starting Point

JVM ergonomics should not be viewed as the final configuration of a production system. Instead, it should be viewed as a starting point. For small applications, development environments, or internal tools, the defaults are often sufficient.

Nowadays, for most of our production systems—cloud-native or on-premise, both running in Kubernetes—the default configuration may leave significant performance improvements unexplored. Relying solely on ergonomics is not necessarily wrong. However, it may indicate that performance has never been evaluated or optimized for the application’s actual workload.

Understanding what the JVM automatically chooses is the first step toward making informed tuning decisions. In the next section, we will walk through some coding examples to understand its choices.

II. Real world example of JVM ergonomics

To see ergonomics in action we don’t need a real application. We just start a JVM and ask it one question: “what did you decide?”. The interesting part is that the answer changes depending on the size of the machine the JVM runs on. So the plan is to run the same JVM on machines of different sizes and watch its decisions change.

We won’t buy several laptops for this. Instead we fake the machine size with Docker. A container lets us cap how much CPU and memory the JVM is allowed to see, so one laptop can pretend to be a tiny 128MB box or a roomy 4GB box. The image we use is an Ubuntu-based Temurin JDK 21 (the version my laptop currently runs, but you can change it).

You can find the full script in the jvm-ergonomics repository. Here is what each piece does.

The program under test is deliberately empty

Hello.java has an empty main. We are not measuring the program — we are measuring the JVM’s decisions. An empty program keeps the measurement clean: nothing allocates memory or spawns threads to distort the defaults.

We fake a machine with a Docker container

docker run --cpus=... --memory=... starts the JVM with exactly the CPU and memory limits we hand it. The script takes those as arguments:

So ./test21.sh 2 2g asks: “on a 2-CPU, 2GB machine, what does the JVM choose?”

We ask the JVM to print its final settings

The flag -XX:+PrintFlagsFinal makes the JVM dump every configuration flag after ergonomics has run — the real values it settled on, not the raw defaults. We never run a workload; the JVM resolves its flags, prints them, and exits. Then we grep for the two things we care about:

(-XX:+AlwaysPreTouch only forces the JVM to actually touch the heap pages it reserves. It does not change the decision — it just makes the reservation real.)

What we are looking for

By running the same script across different --cpus and --memory combinations, we can watch the JVM flip its choices at certain thresholds — a different heap size here, a different collector there. Those crossover points are the ergonomics rules, observed directly instead of read from the docs.

And here is the result:

CPUsMemoryCollectorMaxHeapSize (bytes)MaxHeapSize
1128mUseSerialGC6710886464 MB
1256mUseSerialGC132120576126 MB
1400mUseSerialGC132120576126 MB
1500mUseSerialGC132120576126 MB
21024mUseSerialGC268435456256 MB
2400mUseSerialGC132120576126 MB
22gUseG1GC536870912512 MB
21791mUseSerialGC469762048448 MB
21792mUseG1GC469762048448 MB

So guess what the trend is here? There are actually two separate decisions hiding in this table — how big the heap is, and which collector runs — and each flips at its own threshold.

The heap-size trend

The heap is a percentage of the container memory, but the percentage changes in three bands:

The collector trend

Collector selection is a separate decision from heap size — it depends on whether the box looks server-class, which HotSpot defines as ≥ 2 CPUs AND ≥ 1792 MB memory. Both must hold:

Notice the two decisions are orthogonal: 1791m and 1792m get identical heaps but different collectors. Heap sizing and collector selection are computed independently.

So don’t rely on ergonomics for heap sizing

The heap-size trend has a sharp practical edge, especially in Kubernetes. If you rely on ergonomics for your heap, the JVM only ever claims 25% of the container memory (once you’re above ~512 MB). It’s not that the JVM ignores extra memory — it does scale with the container — but it only takes a quarter of it.

So picture the common scenario: your ops team bumps a pod from 2 GB to 4 GB to “give the app more room.” Kubernetes now reserves 4 GB for that pod, but the JVM heap only grows from 512 MB to 1 GB. The other ~3 GB is reserved in the cluster and paid for, but the JVM never uses it as heap. Everyone assumes the app got more headroom; in reality ~75% of the increase is invisible to the heap.

The takeaway: if you’re not tuning the JVM yourself, at least set the heap explicitly. Go to your configuration and pin -Xmx (or -XX:MaxRAMPercentage) instead of trusting the 25% default. Otherwise a memory change made by someone who doesn’t know about ergonomics silently fails to reach the heap.

One reminder for accuracy: everything in this section — the 50%/25% percentages, the flat 126 MB band, and the 1792 MB server-class threshold — is HotSpot ergonomics behavior, not mandated by the JVM specification. JVMS only guarantees that a garbage-collected heap exists (§2.5.3). These exact numbers are HotSpot’s choices for the Java 21 baseline and can change between versions, which is all the more reason not to rely on them.

III. Conclusion

The heap example above was about one setting. The real lesson is bigger: in a container, don’t run on ergonomics at all. Not even when you’ve read this post and think you know what the JVM will pick — because the values shift between Java versions, and “knowing the default” is not the same as controlling it.

The reason isn’t only technical, it’s operational. In a Kubernetes environment everyone around your app wants precision — what is running, how much memory, how many CPUs:

If the JVM quietly decides for itself, none of these teams can see the truth from the outside. The container says 4 GB; the JVM uses 1 GB; nobody agrees on what the app actually needs.

So the rule of thumb — my advice is: do not java -jar myapp.jar. Running with zero flags hands three important decisions to the defaults. Instead, always set them yourself:

We’ll dig into each of those knobs — how to pick them and how the JVM reads the CPU limit — but not today, maybe another bonus section in the future 😉. W guys 😘.

Related Posts