-Xmx: too small and ZGC burns CPU collecting often, too big and you waste memory that a neighbour on the same host could have used. The second is the initial heap size, -Xms: small starts fast but warms up slowly, large warms up fast but starts slowly. Two proposals for JDK 28, JEP 546 (Adaptive Heap Sizing for ZGC) and JEP 545 (Faster Startup and Warmup with ZGC), aim to remove both decisions.
This post is deliberately split into what the JEPs say and what I could measure, because those turned out to be different things.
Status as of 12 October 2026. Both JEPs show “Proposed to Target” for release 28 on openjdk.org (both last updated 2026/10/07). Neither is integrated. The headline flag,-XX:+ZAdaptiveHeapSizing, is rejected by every JDK I tried, including the newest JDK 28 early-access build. So I could not benchmark the new behaviour, and I have not pretended to. Everything about the proposals below comes from the JEP text; everything with a number comes from my own runs of today’s JDKs.
Versions tested: Temurin 25.0.4.1+1 (LTS), Temurin 27+35 (GA 2026-09-15), OpenJDK 28-ea+19 (jdk.java.net build 19, 2026-10-08), Spring Boot 4.1.1. Machine: 2 vCPU, 8 GB RAM.
First, check whether you can even try it
The quickest honest test is to ask the JVM. This is the output for the JDK 28 early-access build, from 01-flag-probe.txt (the same file shows JDK 25 and 27 behaving identically):== JDK 28-ea
openjdk version "28-ea" 2027-03-23
size_t InitialHeapSize = 8388608 {product} {ergonomic}
size_t MaxHeapSize = 1568669696 {product} {ergonomic}
size_t SoftMaxHeapSize = 1411802726 {manageable} {ergonomic}
bool ZUncommit = true {product} {default}
uintx ZUncommitDelay = 300 {product} {default}
-- probing -XX:+ZAdaptiveHeapSizing
Unrecognized VM option 'ZAdaptiveHeapSizing'
The last line is the point: the flag does not exist yet. When a build does contain it, the harness in this post lets you rerun the same experiments and compare. Until then, the useful work is understanding the problem and measuring the baseline.
The smallest correct mental model: reserve, commit, expand, contract
A ZGC heap is not one block of memory. It is a big promise followed by a series of small purchases. The JEP 545 primer describes it like this: at startup ZGC reserves a range of virtual address space 16 times the maximum heap size, which is only bookkeeping and is fast. It then asks the operating system to commit real physical pages, as many as the initial heap size. Committing is the expensive step, because the OS must find free pages, zero them, and map them. As the application allocates, ZGC expands the heap by committing more. When regions become garbage, it can contract the heap by giving pages back.What JEP 545 proposes: faster startup and warmup
According to the JEP, the initial heap size has a strong effect on both startup and warmup, and the difference can be dramatic: on typical current hardware the JVM starts in about a tenth of a second with a 2 MB initial heap but needs over ten seconds with a 32 GB one. The proposal has three parts. ZGC would start faster by requesting just 2 MB for the heap by default. It would warm up faster by expanding the heap aggressively. And it would manage memory in units of 2 MB rather than 4 KB. The JEP’s own case study of a prototypical server reports almost 40% less startup time and almost 90% less warmup time. I have not reproduced those figures; they are the JEP authors’ numbers.Going deeper: harvesting, remapping and why 2 MB units matter
The JEP primer explains one more trick that is easy to miss. ZGC groups heap pages into regions of 2 MB to 32 MB. When a region holds only garbage, its physical pages can be harvested: ZGC remaps a contiguous range of its huge reserved virtual space onto those physical pages. That lets it find room for a large object, such as a 4 MB array, without running a full collection cycle to compact the heap. The JEP says this is several orders of magnitude faster than compacting. The primer also notes that the JEP’s non-goals include using the same memory techniques on every OS; for example large pages might be used by default on Linux but not on Windows. So expect behaviour to differ by platform when this lands.
What JEP 546 proposes: let ZGC choose the heap size
JEP 546 describes an optional policy, enabled with-XX:+ZAdaptiveHeapSizing. When on, ZGC would use a default maximum heap of 100% of physical memory minus a small reserve (today’s default is 25%, which you can see in the probe above: a 1.46 GiB maximum on this machine). It would adjust the heap to balance memory against GC CPU, and it would act as a good neighbour, expanding or contracting as though a soft maximum had been set, in response to memory and CPU use across the host.
One knob remains: -XX:ZGCIntensity, a positive integer with a default of 5. Lower values collect less often, using less CPU and more memory. Higher values collect more often, using more CPU and less memory. The JEP says the relationship between intensity and real CPU use is intentionally unspecified so it can be tuned in later releases.
The command line the JEP wants you to need.java -XX:+UseZGC -XX:+ZAdaptiveHeapSizing ...with no-Xms, no-Xmxand no-XX:+AlwaysPreTouch. I have not run this: as shown above, no build I could download accepts the flag.
What I could measure: today’s startup baseline
To give the proposals something to be compared against, I built a small Spring Boot 4.1.1 server and measured the wall-clock time from launching the JVM to the first successful HTTP response, five runs per configuration. The harness is in startup.sh and the server is DemoApplication.java. These are the JDK 28 early-access summaries from 02-startup-vs-xms.txt:jdk28ea zgc default SUMMARY: min 3170 ms, median 3420 ms, max 3918 ms (n=5)
jdk28ea zgc Xms64m SUMMARY: min 3217 ms, median 3350 ms, max 3522 ms (n=5)
jdk28ea zgc Xms1g SUMMARY: min 3458 ms, median 3642 ms, max 4136 ms (n=5)
jdk28ea zgc Xms3g SUMMARY: min 3590 ms, median 3803 ms, max 3871 ms (n=5)
jdk28ea zgc Xms3g pretouch SUMMARY: min 4764 ms, median 5010 ms, max 5283 ms (n=5)
AlwaysPreTouch costs the most: about 1.2 seconds more than a plain 3 GB heap here. But the effect is small next to the roughly 3.3 seconds Spring Boot needs anyway, because this machine only has 8 GB of RAM. The dramatic ten-second figure in the JEP belongs to a 32 GB initial heap, which I could not test.
Here is the full median table for all three JDKs, in milliseconds. JDK 27 and 28 early access behave much like JDK 25 on this measure:
| Config | JDK 25 | JDK 27 | JDK 28-ea |
|---|---|---|---|
| ZGC defaults | 3439 | 3421 | 3420 |
-Xms64m | 3299 | 3276 | 3350 |
-Xms1g | 3478 | 3275 | 3642 |
-Xms3g | 3836 | 3618 | 3803 |
-Xms3g -XX:+AlwaysPreTouch | 4834 | 4560 | 5010 |
What I could measure: warmup is not about the heap here
Warmup is the time a server needs to reach its steady speed. I fired 20,000 requests with four client threads and timed each batch of 2,000, using Warmup.java, always on the same JDK 25 client so only the server changes. Excerpt from 03-warmup.txt:== jdk28ea zgc default
batch ms: 2567 1217 977 979 907 826 730 590 602 512
total ms: 9912
committed after load: 284 MiB (max 1496 MiB)
The first batch takes 2.4 to 2.7 seconds in every configuration I ran, including a 3 GB initial heap, and later batches fall steadily to about half a second. That curve is the JIT compiler and class loading warming up, not the heap growing. For a small service on a small machine, JEP 545’s warmup claim has little room to help. Its case is a large heap, where acquiring memory on demand really is slow.
Going deeper: warmup totals for every configuration
Total milliseconds for the first 20,000 requests, with the first batch of 2,000 in brackets (from the same output file):
| Config | JDK 25 | JDK 28-ea |
|---|---|---|
| ZGC defaults | 9708 (2678) | 9912 (2567) |
-Xms3g | 9193 (2606) | 9608 (2431) |
-Xms64m | 9247 (2442) | 9893 (2590) |
-Xms3g the reported maximum rose to 3072 MiB, because an initial heap larger than the default maximum raises the maximum too.
One surprise: the default initial heap already changed
Look back at the probe output: on JDK 28 early access, ZGC’s ergonomicInitialHeapSize is 8,388,608 bytes, 8 MiB. On JDK 25 on the same machine it was 98,566,144 bytes, roughly 94 MiB. JDK 27 shows the 8 MiB figure too. So ZGC’s default start size has already shrunk between JDK 25 and 27. I did not trace which change did this, and it is not JEP 545 (which proposes 2 MB and is not in a build), so I record it as an observation only. If you rely on the default heap at startup, retest when you move from 25 to 27.
Should you wait for these JEPs?
No, but do not tune around them either. Neither JEP is in a released JDK, and an early-access build does not contain the flag. Keep your current container sizing (see the JVM flags cheat sheet) and do not invent a workaround for a problem a future JDK may remove. If you run ZGC with very large heaps, or several JVMs per host, these proposals are worth following. For a small Spring Boot service, my numbers say the heap is not your startup or warmup bottleneck.
Reproduce it, and rerun it when the flag lands
Everything is in the jdk28-zgc module README. In short: build with
mvn -B -DskipTests package, set J25, J27 and J28 to JDK home directories, and run scripts/run-all.sh. The probe in step one will start reporting the flag as recognised the day a build includes it; at that point add -XX:+ZAdaptiveHeapSizing to the configurations in run-all.sh and compare.
This post’s repository has no docs/ folder by design: this article and the module README are the documentation, and the raw run output is in output/. The machine is small (2 vCPU, 8 GB), so the absolute numbers are illustrative.
No Comments yet!