You built a Spring Boot app. It runs perfectly on your laptop. You put it in a Docker container, deploy it, and boom. The container crashes. No error log. No stack trace. Just gone.
The operating system killed your app because it used too much memory. This is called OOM Killed (Out Of Memory Killed).
This happens every day to Java developers who deploy to containers. And it is one of the best interview questions to test if someone understands how Java and Docker really work together.
Here is the full breakdown.
What Is OOM Killed?
When a process uses more memory than the system allows, the Linux kernel kills it. No warning. No graceful shutdown. The process just stops.
In Docker, you can set a memory limit. For example, docker run -m 512m myapp means the container can only use 512 MB of RAM. If your Java app tries to use more than that, the kernel kills it.
You can check if your container was OOM killed by running:
docker inspect <container_id> --format='{{.State.OOMKilled}}'
If this returns true, that is your answer.
Why Does Java Use More Memory in Docker?
This is the core problem. There are three reasons.
1. The JVM Does Not Know About Container Limits
On your laptop, the JVM sees all your RAM. Let us say you have 16 GB. By default, the JVM sets its maximum heap size to about 1/4 of total RAM. That is 4 GB.
But in a Docker container with a 512 MB limit, the JVM still looks at the host machine's total RAM, not the container limit. So it might try to use 4 GB inside a 512 MB container. The kernel kills it.
This was fixed in Java 8u191 and later, but only if you use the right flags.
2. Heap Memory Is Not the Only Memory
Most developers only think about heap memory. That is what -Xmx controls. But the JVM uses a lot more memory than just the heap:
-
Heap: Where your objects live (controlled by
-Xmx) - Metaspace: Where class definitions are stored
- Thread stacks: Each thread gets its own stack (about 1 MB per thread)
- Direct buffers: Used by NIO and Netty
- JIT compiler: Compiles bytecode to machine code
- GC data: Garbage collector needs memory to track objects
A typical Spring Boot app with -Xmx256m might actually use 400-500 MB total. The heap is only part of the story.
3. Spring Boot Uses a Lot of Memory by Default
A fresh Spring Boot app with just spring-boot-starter-web starts with about 200-250 MB of total memory. Add a database connection pool (HikariCP default is 10 connections), some caching, and you are at 350-400 MB easily. And you have not even started your business logic yet.
The Fix: Three Things You Must Do
Fix 1: Use Container-Aware Memory Settings
Stop using -Xmx with fixed values. Instead, use this flag:
java -XX:MaxRAMPercentage=75.0 -jar myapp.jar
This tells the JVM: "Use 75% of whatever memory is available in this container." If the container has 512 MB, the JVM heap will be about 384 MB. If the container has 1 GB, the heap will be about 768 MB.
This is much better than -Xmx256m because it automatically adjusts to the container limit.
Why 75% and not 100%? Because the JVM needs the remaining 25% for non-heap memory (metaspace, threads, GC, etc.).
Fix 2: Set the Docker Memory Limit Higher Than You Think You Need
If your app needs 256 MB of heap, do not set the container to 256 MB. The JVM will need at least 30-40% more for non-heap memory. Set it to at least 400 MB.
# docker-compose.yml
services:
myapp:
image: myapp:latest
deploy:
resources:
limits:
memory: 512M
environment:
- JAVA_OPTS=-XX:MaxRAMPercentage=75.0
Fix 3: Reduce Spring Boot's Memory Appetite
Here are the most effective ways to cut memory usage:
Reduce thread count:
# application.yml
server:
tomcat:
threads:
max: 50 # default is 200
Each Tomcat thread uses about 1 MB of stack memory. 200 threads = 200 MB just for stacks. Most apps do not need 200 threads.
Reduce connection pool:
spring:
datasource:
hikari:
maximum-pool-size: 5 # default is 10
Each database connection uses memory for buffers and tracking. 5 is usually enough for a small app.
Turn off things you do not use:
spring:
jmx:
enabled: false # saves ~20 MB
devtools:
restart:
enabled: false # never use devtools in production
How to Debug Memory Issues in Docker
If your container keeps getting OOM killed and you do not know why, here is a step-by-step approach.
Step 1: Check if it really is OOM
docker inspect <container_id> --format='{{.State.OOMKilled}}'
Step 2: Check actual memory usage
docker stats <container_id>
This shows you how much memory the container is actually using. If it is close to the limit, you found your problem.
Step 3: See what the JVM is doing
docker exec <container_id> jcmd 1 VM.native_memory summary
This shows a breakdown of all JVM memory usage: heap, metaspace, threads, JIT, everything.
You need to enable Native Memory Tracking at startup:
java -XX:NativeMemoryTracking=summary -XX:MaxRAMPercentage=75.0 -jar myapp.jar
Step 4: Check heap usage
docker exec <container_id> jcmd 1 GC.heap_info
This tells you how much heap is used vs. how much is allocated.
A Real Example
Let us say you have a Spring Boot app deployed on a VPS with 1 GB RAM. You run it in Docker with a 512 MB limit.
With default settings:
- JVM sees 1 GB host RAM, sets heap to ~256 MB
- Metaspace: ~80 MB
- Thread stacks (200 default): ~200 MB
- Other JVM overhead: ~60 MB
- Total: ~596 MB in a 512 MB container
- Result: OOM Killed
With our fixes:
-
-XX:MaxRAMPercentage=75.0: Heap = ~384 MB - Reduce threads to 50: ~50 MB
- Reduce connection pool: saves ~30 MB
- Turn off JMX: saves ~20 MB
- Total: ~464 MB in a 512 MB container
- Result: Runs fine with 48 MB headroom
What About Bangladesh VPS Deployments?
Most BD developers deploy to cheap VPS instances with 1-2 GB RAM. They run the database, the app, and sometimes Redis on the same machine. Memory is tight.
Here is a practical setup for a 1 GB VPS running Spring Boot + PostgreSQL:
- PostgreSQL: ~200 MB
- Spring Boot with our fixes: ~450 MB
- OS and other processes: ~200 MB
- Remaining buffer: ~150 MB
This works. Without the fixes, Spring Boot alone would try to use 600+ MB and the whole system would grind to a halt.
Interview Answer Template
If you get this question in an interview, structure your answer like this:
Explain what OOM Killed means: The Linux kernel kills processes that exceed their memory limit.
Explain why Java is special: The JVM uses much more memory than just the heap. Heap is only 50-60% of total JVM memory.
Explain the container problem: By default, the JVM looks at host memory, not container limits. So it allocates more than the container allows.
Give the fix: Use
-XX:MaxRAMPercentage=75.0instead of-Xmx. Set container limits 30-40% higher than heap size. Reduce thread count and connection pool.Show you can debug it: Mention
docker stats,jcmd VM.native_memory, andOOMKilledinspection.
This shows the interviewer you understand the full picture: Java, Docker, Linux, and production reality.
Quick Reference
| Setting | Default | Recommended for Containers |
|---|---|---|
| Max heap | 1/4 of host RAM | -XX:MaxRAMPercentage=75.0 |
| Tomcat threads | 200 | 50 (or less for small apps) |
| HikariCP pool | 10 | 5 (or less for small apps) |
| JMX | enabled | disabled in production |
| DevTools | auto | always disabled in production |
Sources
- OpenJDK documentation: Container awareness in JDK
- Spring Boot reference: Spring Boot Docker
- Baeldung: Docker Memory Limits for Java
- Docker documentation: Runtime options for memory
Top comments (0)