# 3. JVM ergonomics: what the JVM decides from your pod spec [← 2. Probes](02-probes.md) · [Index](../README.md) · Next: [4. CPU limits and GC →](04-cpu-limits-and-gc.md) With no JVM options at all, the JVM reads the container's cgroup limits and picks a processor count, a garbage collector, a heap size and thread counts. [`demo-ergonomics.sh`](../scripts/demo-ergonomics.sh) runs `java -XX:+PrintFlagsFinal -version` in a pod per resource shape ([`jvm-ergonomics.txt`](output/jvm-ergonomics.txt)): ``` cpu.req cpu.lim mem.req mem.lim | CPUs GC MaxHeap PGCThr CGCThr JITThr 100m none 256Mi 2Gi | 2 G1GC 512M 2 1 2 100m 500m 256Mi 512Mi | 1 SerialGC 128M 0 0 2 100m 1 256Mi 1Gi | 1 SerialGC 256M 0 0 2 100m 1 256Mi 2Gi | 1 SerialGC 512M 0 0 2 100m 1500m 256Mi 2Gi | 2 G1GC 512M 2 1 2 100m 2 256Mi 1Gi | 2 SerialGC 256M 0 0 2 100m 2 256Mi 1700Mi | 2 SerialGC 426M 0 0 2 100m 2 256Mi 1800Mi | 2 G1GC 450M 2 1 2 100m 2 256Mi 4Gi | 2 G1GC 1024M 2 1 2 ``` ## The rules the table follows - **CPUs = the limit, rounded up.** 500m → 1, 1500m → 2. With no limit, the JVM sees every CPU on the node - here 2, on a production node perhaps 64. - **Requests are ignored.** Every row has a 100m request. JDK 19 removed CPU shares from the calculation (JDK-8281181), so on a modern JDK `requests.cpu` affects scheduling and nothing the JVM decides. - **G1 needs 2 CPUs *and* about 1792 MB.** Below either, the JVM is not a "server-class machine" and picks SerialGC: a single-threaded, stop-the-world collector. 2 CPUs with 1700Mi → Serial; 1800Mi → G1. A typical Spring Boot pod sized 512Mi-1Gi therefore **runs SerialGC without anyone having chosen it**. - **Heap = 25 % of the memory limit** (`MaxRAMPercentage=25`). A 512Mi pod gets a 128 MB heap and 384 MB for metaspace, code cache, thread stacks and native memory - usually far more than they use. `-XX:MaxRAMPercentage=75` is the usual correction; the manifests here set it through `JAVA_TOOL_OPTIONS`. - **1500m gives G1 two parallel GC threads on one and a half CPUs of quota.** That combination is what [chapter 4](04-cpu-limits-and-gc.md) measures. ## cgroup v1 here, v2 in production The lab node is cgroup v1 (a limitation of the VM it ran in; Kubernetes 1.35+ refuses v1 unless `failCgroupV1=false`). JVM container detection works the same on both. The files differ: | | cgroup v1 | cgroup v2 | |---|---|---| | CPU limit | `cpu/cpu.cfs_quota_us` / `cpu.cfs_period_us` | `cpu.max` (`150000 100000`) | | throttling | `cpu/cpu.stat`: `nr_throttled`, `throttled_time` (ns) | `cpu.stat`: `nr_throttled`, `throttled_usec` | | memory limit | `memory/memory.limit_in_bytes` | `memory.max` | [`JvmController`](../src/main/java/com/ankurm/k8s/diag/JvmController.java) reads either. `java -XshowSettings:system -version` prints what the JVM detected, including the provider: ``` Operating System Metrics: Provider: cgroupv1 Effective CPU Count: 2 CPU Period: 100000us CPU Quota: 150000us ```