Container Awareness
Purpose
Decide whether the JVM's automatic sizing inside this container is the sizing you
actually want. HotSpot's UseContainerSupport — on by default since JDK 10 on supported
Linux builds — incorporates cgroup memory and CPU constraints into ergonomics, using
version-specific detection. It does not make the resulting heap, GC thread
count or JIT thread count right for the workload.
The failure this prevents is the confidently wrong container diagnosis: a pod killed for
non-heap charges while the heap sat at 70%, diagnosed without reconciling memory views;
or a detected CPU count inferred from the default ActiveProcessorCount=-1 sentinel.
Workflow
This is Linux HotSpot guidance, with JDK 17–25 as the main command baseline and a JDK 26
heap-default note in the reference. Inspect the runtime image's vendor/update, launch flags,
deployment resources and kernel/cgroup version; a build toolchain alone does not identify
production ergonomics. Do not upgrade the runtime to match these examples.
- Establish what the target JVM detected, from inside its container. A fresh
java -XshowSettings:system is a probe, not proof of the live JVM's settings: match binary,
options and cgroup, and prefer in-process availableProcessors() plus live flags.
Use -Xlog:os+container=trace for the raw
cgroup reads, jcmd <pid> VM.flags -all for ergonomically resolved flags. See
references/reading-the-container.md.
- Confirm the cgroup version before running any cgroup command. v2 is a unified
hierarchy with different file names and different field names; a v1 command does not
fail loudly on v2, it finds nothing.
- Separate the memory question from the CPU question. They have different evidence:
memory.current / memory.events for one, cpu.max / cpu.stat for the other.
- For a kill, take deltas from the process's actual cgroup. An
oom_kill increment in
cgroup v2 memory.events.local records a member killed by an OOM killer, including a
global OOM killer; it does not prove this cgroup's limit triggered the kill. The
hierarchical memory.events may include descendants. Correlate pod/container status and
timestamps and kernel OOM context to establish victim and cause. Absence routes investigation to runtime, node and
signal evidence in linux-for-jvm.
- Reconcile memory views under load, not at boot, before changing any limit. NMT tracks
many JVM-native reservations/commitments but not all process or cgroup charges, and
committed bytes are not identical to RSS. Compare heap, NMT, RSS/PSS, direct-memory
metrics and cgroup
memory.stat; lowering -Xmx may create headroom at the cost of more
GC, so validate both rather than calling it intrinsically wrong.
- For latency spikes with no matching GC pause, measure throttling:
nr_throttled
over nr_periods from cpu.stat, at peak load, timestamp-correlated with the
client-side spikes.
- Re-measure the metric that motivated the change, under the same load. A container
change is not validated by the absence of the old symptom in a different run.
Rules
- Read
ActiveProcessorCount to discover an explicit override, not an automatically
detected count. It is a HotSpot product flag; default -1 requests automatic detection
and is not rewritten with its result. Use Runtime.getRuntime().availableProcessors()
in the target JVM; java -XshowSettings:system is a Linux probe available on the JDK 17 baseline.
- In
-XshowSettings:system, the answer is the Effective CPU Count field. The
List of Effective Processors, N total line reports an effective processor set, not a
quota-derived count: with unrestricted cpuset under --cpus=2 on a 24-CPU host it can read
Effective CPU Count: 2 and List of Effective Processors, 24 total. Quoting the 24 is the same mistake as reading
the flag, one line lower.
jcmd <pid> VM.flags shows selected non-default flags, including ergonomic choices.
Use -all for the full flag table and origins; neither form converts the automatic
ActiveProcessorCount sentinel into the detected count.
- Use
grep -w when extracting a flag from PrintFlagsFinal. MaxHeapSize without -w
also matches SoftMaxHeapSize and returns the wrong line. The value is field $4 of
<type> <name> = <value> {tags}, in bytes.
- On cgroups v2, controllers share a hierarchy; resolve the target's actual cgroup directory
before reading
cpu.stat, rather than assuming either a /cpu/ controller or mount root.
Both versions use nr_periods and nr_throttled; time is throttled_time (ns) in v1,
throttled_usec in v2.
- Kubernetes CPU requests are scheduler shares and are not a cgroup CPU-capacity value the
JVM should interpret as a hard capacity. Older HotSpot updates nevertheless used shares;
verify the deployed update's behavior. Updated HotSpot derives an effective count from applicable
quota, cpuset/affinity and host constraints (or an explicit
ActiveProcessorCount). A pod
with request 500m and limit 4 can therefore size parallel facilities near four even
though actual CPU service depends on contention and scheduler weights.
- Require an explicit memory capacity policy. If the container has no memory limit, inspect
inherited cgroup constraints and live heap sizing; host memory may drive ergonomics. Do
not infer an exact 25% heap or absent effective limits from a missing manifest block alone.
- Never set
-Xmx numerically equal to limits.memory. That leaves zero headroom for
everything that is not heap.
- Reject any fixed multiplier over
Xmx as a universal memory-limit rule. Native footprint
is workload-dependent. Size from correlated heap, NMT, process RSS/PSS and cgroup charges
at representative peaks, with restart/dump/traffic transients included.
- Treat
MaxRAMPercentage=90 as a high-risk hypothesis, not automatically a bug. It can be
viable for a simple low-native-footprint process and disastrous for many threads, direct
buffers or agents. Absolute headroom and kill probability decide; no 60–70% default is an
answer either.
- CPU bandwidth exhaustion can deschedule application, GC and JIT work governed by that
quota. It is not a distinct GC event, but can lengthen a GC pause's recorded wall time.
Do not require the absence of a GC pause before investigating throttling; ancestor quotas
and per-CPU runtime accounting also matter.
- Confirm the JDK version against the cluster's cgroup version before trusting detection.
Cgroups v2 support landed in JDK 15 and was backported, including to 11.0.16. Major version
alone cannot establish support or exclude later detection bugs; verify vendor/update and logs.
- Collecting NMT only at boot proves nothing about a kill under load. Take the summary at
peak.
Deliver the target/runtime/cgroup identity, measured memory or CPU evidence, competing
explanations, proposed change and same-load validation metric. Missing access or counters
leave the diagnosis conditional; an empty command output is not a healthy reading.
References
- Reading the container — the commands that answer
each detection question, the cgroup v1 to v2 file and field map, and the
kubectl exec
forms. Read before running any diagnostic inside a container, and whenever a cgroup path
returns nothing.
- Sizing heap and CPU limits — the fixed
-Xmx
versus MaxRAMPercentage decision table, the NMT headroom procedure, and the throttling
measurement procedure. Read when choosing or changing resources.limits, or when
deciding whether a latency problem is a CPU-quota problem.
1---2name: container-awareness3description: What the JVM actually detects inside a container: cgroup v1 versus v2 detection, ActiveProcessorCount and how a CPU quota becomes a processor count, MaxRAMPercentage and every ergonomic derived from it, GC and JIT thread counts sized from the wrong number, and verifying all of it from inside the running container. Use when a pod is OOMKilled while heap usage is well below Xmx, when a Deployment has no resources.limits or sets limits.memory equal to Xmx, when MaxRAMPercentage is pushed to 90, when someone reads ActiveProcessorCount out of PrintFlagsFinal or jcmd VM.flags and gets -1, when a cgroup command reads /sys/fs/cgroup/cpu/cpu.stat and finds nothing, or when latency spikes have no matching GC pause. Does not cover host-side kernel behaviour such as the node OOM killer, page faults, swap, PSI or signals (linux-for-jvm), the memory-region budget itself (jvm-memory-regions), or CPU topology and pinning (numa-and-cpu-affinity).4---56# Container Awareness78## Purpose910Decide whether the JVM's automatic sizing inside this container is the sizing you11actually want. HotSpot's `UseContainerSupport` — on by default since JDK 10 on supported12Linux builds — incorporates cgroup memory and CPU constraints into ergonomics, using13version-specific detection. It does not make the resulting heap, GC thread14count or JIT thread count right for the workload.1516The failure this prevents is the confidently wrong container diagnosis: a pod killed for17non-heap charges while the heap sat at 70%, diagnosed without reconciling memory views;18or a detected CPU count inferred from the default `ActiveProcessorCount=-1` sentinel.1920## Workflow2122This is Linux HotSpot guidance, with JDK 17–25 as the main command baseline and a JDK 2623heap-default note in the reference. Inspect the runtime image's vendor/update, launch flags,24deployment resources and kernel/cgroup version; a build toolchain alone does not identify25production ergonomics. Do not upgrade the runtime to match these examples.26271. **Establish what the target JVM detected, from inside its container.** A fresh28 `java -XshowSettings:system` is a probe, not proof of the live JVM's settings: match binary,29 options and cgroup, and prefer in-process `availableProcessors()` plus live flags.30 Use `-Xlog:os+container=trace` for the raw31 cgroup reads, `jcmd <pid> VM.flags -all` for ergonomically resolved flags. See32 `references/reading-the-container.md`.332. **Confirm the cgroup version before running any cgroup command.** v2 is a unified34 hierarchy with different file names _and_ different field names; a v1 command does not35 fail loudly on v2, it finds nothing.363. **Separate the memory question from the CPU question.** They have different evidence:37 `memory.current` / `memory.events` for one, `cpu.max` / `cpu.stat` for the other.384. **For a kill, take deltas from the process's actual cgroup.** An `oom_kill` increment in39 cgroup v2 `memory.events.local` records a member killed by an OOM killer, including a40 global OOM killer; it does not prove this cgroup's limit triggered the kill. The41 hierarchical `memory.events` may include descendants. Correlate pod/container status and42 timestamps and kernel OOM context to establish victim and cause. Absence routes investigation to runtime, node and43 signal evidence in `linux-for-jvm`.445. **Reconcile memory views under load**, not at boot, before changing any limit. NMT tracks45 many JVM-native reservations/commitments but not all process or cgroup charges, and46 committed bytes are not identical to RSS. Compare heap, NMT, RSS/PSS, direct-memory47 metrics and cgroup `memory.stat`; lowering `-Xmx` may create headroom at the cost of more48 GC, so validate both rather than calling it intrinsically wrong.496. **For latency spikes with no matching GC pause, measure throttling**: `nr_throttled`50 over `nr_periods` from `cpu.stat`, at peak load, timestamp-correlated with the51 client-side spikes.527. **Re-measure the metric that motivated the change, under the same load.** A container53 change is not validated by the absence of the old symptom in a different run.5455## Rules5657- Read `ActiveProcessorCount` to discover an explicit override, not an automatically58 detected count. It is a HotSpot product flag; default `-1` requests automatic detection59 and is not rewritten with its result. Use `Runtime.getRuntime().availableProcessors()`60 in the target JVM; `java -XshowSettings:system` is a Linux probe available on the JDK 17 baseline.61- In `-XshowSettings:system`, the answer is the **`Effective CPU Count`** field. The62 `List of Effective Processors, N total` line reports an effective processor set, not a63 quota-derived count: with unrestricted cpuset under `--cpus=2` on a 24-CPU host it can read64 `Effective CPU Count: 2` and `List of Effective Processors, 24 total`. Quoting the `24` is the same mistake as reading65 the flag, one line lower.66- `jcmd <pid> VM.flags` shows selected non-default flags, including ergonomic choices.67 Use `-all` for the full flag table and origins; neither form converts the automatic68 `ActiveProcessorCount` sentinel into the detected count.69- Use `grep -w` when extracting a flag from `PrintFlagsFinal`. `MaxHeapSize` without `-w`70 also matches `SoftMaxHeapSize` and returns the wrong line. The value is field `$4` of71 `<type> <name> = <value> {tags}`, in bytes.72- On cgroups v2, controllers share a hierarchy; resolve the target's actual cgroup directory73 before reading `cpu.stat`, rather than assuming either a `/cpu/` controller or mount root.74 Both versions use `nr_periods` and `nr_throttled`; time is `throttled_time` (ns) in v1,75 `throttled_usec` in v2.76- Kubernetes CPU requests are scheduler shares and are not a cgroup CPU-capacity value the77 JVM should interpret as a hard capacity. Older HotSpot updates nevertheless used shares;78 verify the deployed update's behavior. Updated HotSpot derives an effective count from applicable79 quota, cpuset/affinity and host constraints (or an explicit `ActiveProcessorCount`). A pod80 with request `500m` and limit `4` can therefore size parallel facilities near four even81 though actual CPU service depends on contention and scheduler weights.82- Require an explicit memory capacity policy. If the container has no memory limit, inspect83 inherited cgroup constraints and live heap sizing; host memory may drive ergonomics. Do84 not infer an exact 25% heap or absent effective limits from a missing manifest block alone.85- Never set `-Xmx` numerically equal to `limits.memory`. That leaves zero headroom for86 everything that is not heap.87- Reject any fixed multiplier over `Xmx` as a universal memory-limit rule. Native footprint88 is workload-dependent. Size from correlated heap, NMT, process RSS/PSS and cgroup charges89 at representative peaks, with restart/dump/traffic transients included.90- Treat `MaxRAMPercentage=90` as a high-risk hypothesis, not automatically a bug. It can be91 viable for a simple low-native-footprint process and disastrous for many threads, direct92 buffers or agents. Absolute headroom and kill probability decide; no 60–70% default is an93 answer either.94- CPU bandwidth exhaustion can deschedule application, GC and JIT work governed by that95 quota. It is not a distinct GC event, but can lengthen a GC pause's recorded wall time.96 Do not require the absence of a GC pause before investigating throttling; ancestor quotas97 and per-CPU runtime accounting also matter.98- Confirm the JDK version against the cluster's cgroup version before trusting detection.99 Cgroups v2 support landed in JDK 15 and was backported, including to 11.0.16. Major version100 alone cannot establish support or exclude later detection bugs; verify vendor/update and logs.101- Collecting NMT only at boot proves nothing about a kill under load. Take the summary at102 peak.103104Deliver the target/runtime/cgroup identity, measured memory or CPU evidence, competing105explanations, proposed change and same-load validation metric. Missing access or counters106leave the diagnosis conditional; an empty command output is not a healthy reading.107108## References109110- [Reading the container](references/reading-the-container.md) — the commands that answer111 each detection question, the cgroup v1 to v2 file and field map, and the `kubectl exec`112 forms. Read before running any diagnostic inside a container, and whenever a cgroup path113 returns nothing.114- [Sizing heap and CPU limits](references/sizing-heap-and-cpu.md) — the fixed `-Xmx`115 versus `MaxRAMPercentage` decision table, the NMT headroom procedure, and the throttling116 measurement procedure. Read when choosing or changing `resources.limits`, or when117 deciding whether a latency problem is a CPU-quota problem.