How do you choose org.gradle.jvmargs heap/metaspace values across local machines and CI agents, and what trade-offs guide the numbers?
answer
- memory budget: daemon + workers + overhead <= RAM
- committed baseline sized for CI/lowest-spec
- ~/.gradle for per-machine overrides
- watch container cgroup limits / OOM-kill
- measure peak, set Xmx above it not 10x
basics
~20 sCommit a sane baseline in the project gradle.properties that fits the smallest target (CI agent), and let powerful dev machines bump it via ~/.gradle/gradle.properties. Size so daemon heap plus forked workers fit RAM, leaving OS headroom.
solid answer
~50 sTreat `org.gradle.jvmargs` as a **budget**, not a single magic number. The committed project-root value should be a baseline that the **lowest-spec target** (usually the CI agent/container) can satisfy, because that file applies everywhere. Developers with more RAM can override locally in `~/.gradle/gradle.properties`. The hard constraint on any machine is: `daemon heap (-Xmx) + (max parallel workers x worker heap) + OS/native overhead <= physical RAM`. Over-allocating the daemon on a constrained CI box causes swapping or container OOM-kill, which is slower and flakier than a smaller heap. Add `-XX:MaxMetaspaceSize` so long-lived agents don't leak metaspace unbounded, and `-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath` so CI failures leave artifacts. Validate empirically: watch peak heap/GC for a clean full build, set `-Xmx` comfortably above the peak, and re-check against `--max-workers` and `Test` worker heaps so the totals fit the smallest agent.
code
toml · 6 lines# project gradle.properties — baseline for CI agent
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m \
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=build/heapdumps
# ~/.gradle/gradle.properties — dev override on a big machine
# org.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=1ggo deeper
Know the committed baseline should fit CI and devs can override locally.
Explain the daemon-plus-workers memory budget and the project vs user-home split.
Reason about cgroup/container OOM-kill, empirical sizing from peak heap, and balancing with --max-workers.
Standardize sizing policy and diagnostics across repos and CI agent classes, with documented budgets per agent size and a measurement-driven update process.
## Frame it as a memory budget The number isn't 'as big as possible'. On any machine the working set is: ``` daemon -Xmx + (parallel forked workers x worker -Xmx) + metaspace + native + OS ``` Gradle can run tasks in parallel and fork test/compiler workers, each its own JVM with its own heap. If you size the daemon greedily and ignore the workers, total demand exceeds RAM, the OS swaps (orders of magnitude slower) or a container OOM-kills the daemon. So the daemon `-Xmx` must coexist with `--max-workers` and the `Test` task heaps. ## Two surfaces, two purposes - **Project-root `gradle.properties` (committed):** a baseline everyone gets — CI and all devs. Because it applies to the **lowest-spec** environment (often the CI container), set it to what the smallest target can satisfy. - **`~/.gradle/gradle.properties` (per-machine, not committed):** local overrides. A dev with 64 GB can raise `-Xmx`; the committed baseline stays conservative. This split avoids the trap of committing an `-Xmx` tuned for a beefy laptop that then OOM-kills the 4 GB CI container. ## CI-specific concerns - **Container limits.** Many CI agents run in cgroup-limited containers. An `-Xmx` near or above the container limit risks the kernel OOM-killing the daemon mid-build. Leave headroom below the container memory limit for native + metaspace + workers. - **Ephemeral daemons.** CI often runs `--no-daemon` or fresh agents, so the metaspace-leak risk is lower there than on a developer's persistent daemon — but a `MaxMetaspaceSize` cap is still cheap insurance and makes failures legible. - **Diagnostics as artifacts.** `-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=<dir under build/>` lets a failing CI job archive the dump for offline analysis. ## How to pick the numbers 1. Run a **clean full build** of the largest module set and observe peak heap and GC behavior (build scans, `-verbose:gc`, or a profiler). 2. Set `-Xmx` comfortably above the observed peak (headroom for spikes), not 10x it. 3. Compute total footprint with your `--max-workers` and worker heaps; ensure it fits the **smallest** agent. 4. Cap metaspace above a clean build's class count plus headroom. 5. Re-measure; adjust. ## Example ```properties # Committed baseline sized for the CI container org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m \ -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=build/heapdumps ``` A developer override: ```properties # ~/.gradle/gradle.properties on a 32 GB workstation org.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=1g ``` ## The trade-off in one line Too small -> OOM and GC thrash; too large -> swapping / container kill / starved workers. The right value is the smallest heap that comfortably holds the build's peak working set on the smallest target environment.
- Why not just commit -Xmx8g so nobody ever OOMs?On a 4 GB CI container that exceeds the limit; the kernel/cgroup OOM-kills the daemon (or it swaps), making builds slower and flakier. The committed baseline must fit the smallest target; big machines override locally.
- You raised the daemon heap and now parallel tests randomly fail with OOM-kill on CI. What happened?Total footprint (daemon + forked test workers x their heap) now exceeds the container's memory limit. Rebalance: lower daemon -Xmx, reduce --max-workers, or trim test-worker maxHeapSize so the sum fits.
saying these in an interview costs you the question
- Committing an -Xmx tuned for a powerful laptop that breaks constrained CI.
- Ignoring forked-worker heaps when budgeting daemon memory.
- Assuming more heap is always faster — past the working set it only invites swapping.