Most Spring Boot apps run with default JVM settings that aren't optimized for containerized production. Learn the JVM flags that improve throughput, reduce GC pauses, and prevent OOM kills.
JOptimize Team
A Spring Boot app deployed with default JVM settings is leaving performance on the table. The JVM doesn't know it's running in a container. It picks a GC algorithm based on hardware it can't see. It sizes the heap for a machine 8x larger than your pod limit. These defaults cause OOM kills, long GC pauses, and unnecessary latency.
# WITHOUT these flags — JVM reads host memory (e.g., 64GB on EC2) # Your pod limit is 2GB → JVM allocates 16GB heap → OOM killed immediately -XX:+UseContainerSupport # Read container memory limits (enabled by default Java 11+) -XX:MaxRAMPercentage=75.0 # Use 75% of container memory for heap -XX:InitialRAMPercentage=50.0 # Start heap at 50% to avoid slow growth -XX:MinRAMPercentage=25.0 # For a 2GB container pod: # -Xmx = 1.5GB (75% of 2GB) # -Xms = 1GB (50% of 2GB) # Remaining 500MB: Metaspace, thread stacks, off-heap, OS
Never hardcode -Xmx in containerized deployments — MaxRAMPercentage adapts to the pod limit automatically.
# G1GC — default for Java 11-20, good general purpose -XX:+UseG1GC -XX:MaxGCPauseMillis=200 # Target GC pause < 200ms -XX:G1HeapRegionSize=16m # For heaps > 2GB # ZGC — best for latency-sensitive services (Java 21) -XX:+UseZGC -XX:+ZGenerational # Generational ZGC (Java 21, significant improvement) # ZGC pauses: typically < 1ms regardless of heap size # Trade-off: slightly higher CPU usage than G1GC # Shenandoah — alternative ultra-low-pause GC -XX:+UseShenandoahGC
Rule: use G1GC (default) for most apps. Switch to ZGC for services with > 4GB heap or strict P99 latency requirements (< 10ms).
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump-$(hostname)-$(date +%Y%m%d-%H%M%S).hprof -XX:+ExitOnOutOfMemoryError # Exit cleanly, let Kubernetes restart # Without ExitOnOutOfMemoryError, app hangs in degraded state after OOM
-XX:+ExitOnOutOfMemoryError ensures the JVM exits immediately on OOM, allowing Kubernetes to restart the pod cleanly rather than leaving a degraded zombie process.
# Prevent unbounded Metaspace growth (e.g., code generation frameworks like Mockito, Hibernate) -XX:MaxMetaspaceSize=256m # Default: unlimited — set a cap -XX:MetaspaceSize=128m # Initial size — avoid resize overhead # For apps with many dynamically loaded classes (plugin systems, CGLIB proxies) -XX:MaxMetaspaceSize=512m
Hibernate, Spring AOP, and Jackson all generate classes at runtime. Without MaxMetaspaceSize, a Metaspace leak causes an OOM in native memory outside the heap — harder to diagnose.
# Tiered compilation (default) — fast startup + optimized hot methods -XX:+TieredCompilation # Default on — don't disable # Class Data Sharing — save JVM startup classes to shared archive # Run once to create archive: java -Xshare:dump -XX:SharedArchiveFile=/app/app-cds.jsa -jar app.jar # Run with archive: -XX:+UseSharedSpaces -XX:SharedArchiveFile=/app/app-cds.jsa # Cuts startup time 30-50% for Spring Boot apps # Spring AOT (Spring Boot 3) — further startup optimization -Dspring.aot.enabled=true
FROM eclipse-temurin:21-jre-alpine WORKDIR /app # ... copy layers ... ENV JAVA_OPTS="" ENTRYPOINT ["sh", "-c", "java \ -XX:+UseContainerSupport \ -XX:MaxRAMPercentage=75.0 \ -XX:InitialRAMPercentage=50.0 \ -XX:+UseZGC -XX:+ZGenerational \ -XX:MaxMetaspaceSize=256m \ -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/tmp/heapdump.hprof \ -XX:+ExitOnOutOfMemoryError \ -Dspring.aot.enabled=true \ -Djava.security.egd=file:/dev/./urandom \ $JAVA_OPTS \ org.springframework.boot.loader.launch.JarLauncher"]
-Djava.security.egd=file:/dev/./urandom prevents SecureRandom from blocking on /dev/random during startup — a common cause of Spring Boot startup hangs in containers.
# deployment.yaml resources: requests: memory: "512Mi" # Guaranteed memory cpu: "250m" # 0.25 CPU cores guaranteed limits: memory: "1Gi" # OOM kill if exceeded cpu: "1000m" # 1 CPU core max (throttled if exceeded)
With -XX:MaxRAMPercentage=75.0 and limits.memory=1Gi:
-XX:+UseContainerSupport (Java 8 < 8u191), JVM reads host memory and allocates a huge heap, causing immediate OOM-Xmx hardcoded in container — if the pod limit changes, hardcoded -Xmx doesn't adapt; MaxRAMPercentage is always correctMaxMetaspaceSize — a Metaspace leak is silent until it crashes the JVM with a native OOM; always cap itExitOnOutOfMemoryError — a JVM running after OOM is in undefined state; Kubernetes should restart it cleanlyProduction JVM configuration for Spring Boot in containers: use MaxRAMPercentage=75 instead of -Xmx, enable ZGC for latency-sensitive services, cap Metaspace, always enable heap dump on OOM with ExitOnOutOfMemoryError, and use CDS for startup optimization. These settings take 10 minutes to add and prevent the most common production JVM failures.
JOptimize connects to your running JVM and shows heap usage, GC frequency, and slowest methods in real-time — see the impact of JVM tuning immediately.
Tune your JVM with real data — free live profiling.
Master Spring Boot, security, and Java performance with hands-on courses.
JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.