Resource limits & JVM tuning¶
Brewlet maps Kubernetes CPU/memory limits to the sandbox cgroup and lets the
container-aware JVM react — it injects no -XX flags of its own. This page
explains exactly what the limits do and how to tune the JVM correctly.
Related: Launchers · Deploying workloads.
The mapping¶
The deployment descriptor's CPU/memory drive the sandbox cgroup only. Modern JDKs are cgroup-v2 aware and read those limits directly.
| Descriptor field | Cgroup effect (via runc) | JVM effect |
|---|---|---|
resources.limits.memory |
memory.max |
-XX:+UseContainerSupport (default on) reads the cgroup and sizes the heap. |
resources.limits.cpu |
cpu.max (quota/period) |
The cgroup-aware JDK auto-detects available processors from the quota; GC/JIT thread counts scale accordingly. |
You can see this for real in integration-test tier 3. Its Linux/runc harness pins
--cpus=1 --memory=384m, and the app reports availableProcessors = 1 and
memory.max = 384Mi from inside the JVM. See
Getting started.
cgroup v2 is required. The provisioner refuses cgroup v1-only nodes, so the container-awareness above always holds on a ready node.
Who tunes the JVM¶
Brewlet never injects tuning flags. Tuning is either yours or your launcher's:
- Vanilla
java→ you tune heap/GC/agents/container flags via descriptorjvm.args. The artifact carries only app-intrinsic correctness knobs such as preview features, module access, and system properties. jaz→ the launcher derives sensible ergonomics (heap/GC/CPU) from the cgroup limits, so you usually pass no tuning flags. See Launchers.
Precedence for the vanilla path: artifact structured launch knobs
(--enable-preview, --add-modules, --add-opens, --add-exports, sorted
-D flags) → descriptor jvm.args (append/override where the JVM honors
last-wins).
Recommended tuning (vanilla java)¶
Memory — leave headroom for non-heap¶
The container memory limit must cover more than the heap: Metaspace, thread
stacks, JIT code cache, direct/mmap buffers, and GC structures all live outside
-Xmx. If you size the heap to 100% of the limit, the JVM (or the kernel OOM killer)
will kill the pod.
Set a percentage that reserves headroom (commonly ~25%):
MaxRAMPercentage is preferred over a fixed -Xmx because it tracks whatever
limits.memory you set — resize the pod and the heap follows.
Fail fast on OOM¶
Let a memory-exhausted JVM exit cleanly so the kubelet restarts it:
GC selection¶
Pick a collector suited to your workload; the JVM will size its GC threads from the CPU quota:
A solid default set¶
jvm:
version: 21
launcher: java
args:
- "-XX:MaxRAMPercentage=75.0"
- "-XX:+UseZGC"
- "-XX:+ExitOnOutOfMemoryError"
Or hand it all to jaz and pass nothing:
CPU limits and threads¶
- With a CPU limit,
cpu.maxsets a quota; the JVM computesavailableProcessors()from it and scales GC, JIT compiler, and commonForkJoinPoolthread counts. - With only a CPU request (no limit), the JVM sees the node's full CPU count — which may over-provision internal thread pools on a busy node. Set a limit if you want deterministic sizing.
- Fractional limits (e.g.
cpu: "1500m") are honored via the quota; the JVM rounds processor counts as it sees fit.
Ports¶
ports is a deployment concern, not an artifact field. It lives in the
descriptor (CRD spec.ports or the Maven manifest goal's <ports>), where the
operator uses it to wire the Service and probes. The artifact's launch config
carries no ports.
Brewlet does not translate a port into any JVM system property — the listen
port is a framework concern (e.g. Spring Boot's server.port, Quarkus's
quarkus.http.port), so configure it the way your framework expects, via env
or extra args (-- -Dserver.port=8080) on a local run. The local run path
does not touch ports at all.
Startup performance¶
Cold start is the JVM's classic weakness vs. Wasm. Brewlet mitigates it with:
- Shared, pre-warmed JDK on the node → no per-pod JDK pull/unpack.
- Artifact caching: containerd's content store caches the JAR layer; only the (small) JAR moves over the network, not a full image.
- AppCDS / dynamic CDS: ships a class-data archive as a dedicated
cds.layer— build-time via the Mavenbrewlet:appcdsgoal /brewlet push --appcds-archive, plus opt-in node-side regeneration — to cut startup. See AppCDS.
See SPECIFICATION §13.
Checklist¶
- [ ] Set
resources.limits.memoryandlimits.cpu. - [ ] Vanilla
java: set-XX:MaxRAMPercentage(reserve non-heap headroom) and-XX:+ExitOnOutOfMemoryError; pick a GC. Or usejazand set nothing. - [ ] Don't expect Brewlet to add flags — it doesn't.
- [ ] Remember the
RuntimeClassoverhead.podFixedaccounts for baseline JVM overhead in scheduling/quotas (Configuration).