Tuesday, October 27, 2026

Kubernetes Requests and Limits for Java Apps: Avoiding OOMKilled and CPU Throttling

“The pod keeps getting OOMKilled but the heap is only 512 MB.” “Latency doubles every few seconds and the CPU graph looks fine.” Both of these come from the same root cause: Kubernetes resource settings that don't match how the JVM actually uses memory and CPU.

Memory: the heap is not the whole process

A Java process uses heap plus metaspace, thread stacks, the code cache, GC structures and direct buffers. If you set -Xmx512m and a 512Mi container limit, the kernel will kill the container long before Java ever throws an OutOfMemoryError.

Let the JVM size itself from the container limit instead:

env:
- name: JAVA_TOOL_OPTIONS
  value: "-XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError"
resources:
  requests:
    memory: "1Gi"
    cpu: "500m"
  limits:
    memory: "1Gi"
  • 75% leaves room for non-heap memory. For apps with many threads or heavy off-heap use (Netty, large caches) go to 60–65%.
  • Memory request = memory limit. Memory is not compressible; a pod that bursts above its request is the first to be evicted when the node is under pressure.
  • ExitOnOutOfMemoryError makes a heap OOM a clean restart instead of a half-dead JVM.

CPU: limits cause throttling

CPU limits are enforced in 100 ms windows. A JVM with several GC and JIT threads can use its whole quota in the first 20 ms and then sit throttled for 80 ms. That shows up as random latency spikes even though average CPU looks low.

  • Set a realistic CPU request so the scheduler places pods sensibly.
  • Either don't set a CPU limit, or set it well above the request (2–4×) for latency-sensitive services.
  • Watch container_cpu_cfs_throttled_periods_total in Prometheus; if it climbs, the limit is too tight.
  • The JVM sizes thread pools from the CPU count it sees. If needed, pin it with -XX:ActiveProcessorCount=2.

Finding the right numbers

  1. Run under realistic load and watch kubectl top pod plus JVM metrics from Actuator/Micrometer.
  2. Install the Vertical Pod Autoscaler in recommendation-only mode and read its suggestions for a week.
  3. Set requests around normal peak usage, then revisit after big releases.

Quick diagnosis

  • Reason: OOMKilled, exit code 137 – container limit hit; lower MaxRAMPercentage or raise the limit.
  • java.lang.OutOfMemoryError in logs – heap is too small for the workload, or there is a leak.
  • Latency spikes, CPU looks low – check throttling before you blame the code.

No comments: