← Docker articles

ACE-U field notes

Container limits and the JVM: why your heap is not your memory limit

A practical look at memory and CPU limits for Java services: sizing the heap, surviving back-pressure and recognising an out-of-memory kill.

Shubham Anandani··5 min read

Problem

Without limits, a hot container can starve its neighbours. With limits that are too tight, it gets killed and restarts in a loop. Either way, the failure spreads: one tier is slow, upstream timeouts fire, retries pile up, and a small problem becomes an outage. On the JVM there is an extra trap. The heap is only part of the memory a process uses. Metaspace, thread stacks, the code cache, direct buffers and GC structures all live outside it, so a container limit equal to the heap size is almost guaranteed to be exceeded.

Options and trade-offs

  • No limits: nothing gets killed by the runtime, but one runaway service can degrade the whole host.
  • A hard limit with a fixed -Xmx: predictable, but easy to get wrong when the limit changes and the heap flag does not.
  • A hard limit with a percentage-based heap: the JVM sizes the heap from the container limit, and you leave headroom for everything else.

My recommendation

Set a memory limit, let the JVM size its heap from it with a percentage, and leave real headroom for native memory. Bound every queue so overload becomes a fast, explicit refusal instead of a slow death. Make the out-of-memory case easy to recognise.

Worked example

The Compose file sets the limit and tells the JVM to use 70 percent of it for the heap. The rest is headroom.

services:
  api:
    image: demo/api:1.4.2
    ports:
      - "127.0.0.1:8080:8080"
    environment:
      JAVA_TOOL_OPTIONS: "-XX:MaxRAMPercentage=70 -XX:+ExitOnOutOfMemoryError"
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M

A tiny program shows what the JVM actually sees inside the container, which is the first thing to check when sizing goes wrong:

public class WhatDoIHave {
    public static void main(String[] args) {
        Runtime rt = Runtime.getRuntime();
        System.out.printf("processors=%d maxHeap=%dMB%n",
                rt.availableProcessors(), rt.maxMemory() / (1024 * 1024));
    }
}

For back-pressure, use a bounded queue and refuse work when it is full, instead of accepting everything and falling over later:

import java.util.concurrent.*;

public class BoundedWorkers {
    private final ThreadPoolExecutor pool = new ThreadPoolExecutor(
            4, 4, 0L, TimeUnit.MILLISECONDS,
            new ArrayBlockingQueue<>(100),
            new ThreadPoolExecutor.AbortPolicy());

    /** Returns an HTTP-style status: 202 accepted, 503 over capacity. */
    public int submit(Runnable task) {
        try {
            pool.execute(task);
            return 202;
        } catch (RejectedExecutionException e) {
            return 503;
        }
    }
}

When a container disappears with exit code 137, check whether the kernel killed it:

docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' api
docker stats --no-stream api

Checklist

  1. Set memory and CPU limits, and write down why each number was chosen.
  2. Use a heap percentage, and leave headroom for native memory.
  3. Bound every queue, and return a clear over-capacity response when it is full.
  4. Log queue depth, rejected work and memory use so the problem is visible before the kill.
  5. Load test with the real limits applied, and watch what the upstream tier does.

Where this doesn't apply

These numbers are starting points for a small service. Large heaps, many threads or heavy direct-buffer use all change the headroom you need, and only measurement on your own workload settles it.

One honest limitation

Limits shrink the blast radius but do not prevent incidents. A service that is out of memory still fails; the point is that it fails predictably and recovers quickly.

Sources

  • Docker Docs: Resource constraints
  • Oracle Java Documentation: Java in containers (JVM ergonomics)
  • Docker Docs: docker container inspect