Java 11 Production Checklist and Performance Best Practices
Production Readiness Checklist
[ ] JDK distribution chosen and version pinned
[ ] Heap and Metaspace sized correctly
[ ] GC selected and tuned for your workload
[ ] Container-aware JVM flags set
[ ] AppCDS archive built for faster startup
[ ] JFR always-on recording configured
[ ] GC logging enabled with rotation
[ ] Security-related algorithms locked down
[ ] Thread and connection pool sizes verified
[ ] JVM exit flags prevent silent crashes
Baseline JVM Flags for Java 11
Start with these flags and tune from here:
java \
# Heap
-Xms2g -Xmx4g \
\
# GC — G1 is the default; make it explicit
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:G1HeapRegionSize=16m \
\
# Metaspace
-XX:MetaspaceSize=256m \
-XX:MaxMetaspaceSize=512m \
\
# OOM behaviour — exit so the process manager can restart
-XX:+ExitOnOutOfMemoryError \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/app/heapdump.hprof \
\
# GC logging
-Xlog:gc*,gc+heap=debug,safepoint:\
file=/var/log/app/gc.log:time,uptime,pid:\
filecount=5,filesize=20m \
\
# Flight Recorder (always on)
-XX:StartFlightRecording=disk=true,\
maxsize=100m,\
maxage=6h,\
settings=default \
\
# Container support
-XX:+UseContainerSupport \
\
-jar myapp.jar
Heap Sizing
General rules
- Initial heap (
-Xms): Set equal to max heap to avoid GC pauses during heap growth. - Max heap (
-Xmx): Leave at least 1 GB for the OS, JVM native memory, and non-heap spaces. On a 4 GB container, set-Xmx3g. - Live data set: Set max heap to at least 3× the live data set size at peak load. Measure with
jcmd <pid> GC.run && jcmd <pid> VM.native_memory.
# Measure heap usage after a forced GC
jcmd <pid> GC.run
jcmd <pid> VM.native_memory summary scale=MB
Detecting sizing problems
| Symptom | Likely Cause | Action |
|---|---|---|
| Frequent young GC (every <1s) | Eden too small | Increase heap |
| Long Full GC pauses | Heap too small, or too much long-lived data | Increase heap, check for memory leaks |
OutOfMemoryError: Java heap space | Max heap exhausted | Increase -Xmx, fix memory leak |
OutOfMemoryError: Metaspace | Too many classloaders or dynamic proxy classes | Increase -XX:MaxMetaspaceSize |
G1GC Tuning
G1GC is the default in Java 11. Its main tuning lever is the pause time goal:
-XX:MaxGCPauseMillis=200 # Default: 200ms. Lower for latency-sensitive apps.
G1 adjusts region set sizes and GC frequency to try to stay below this target. It is a target, not a hard guarantee.
Important G1 parameters
# Region size — auto-computed but can be forced
# Should be set so the heap contains 2048 regions
# For 8GB heap: 8192 MB / 2048 = 4 MB (default)
# For 16GB heap: 16384 MB / 2048 = 8 MB
-XX:G1HeapRegionSize=8m
# Trigger mixed GC when old gen exceeds this % of heap
-XX:InitiatingHeapOccupancyPercent=45 # default
# Maximum percentage of heap for survivor spaces
-XX:G1MaxNewSizePercent=60 # default
# Parallel GC threads (default: num CPUs)
-XX:ParallelGCThreads=8
# Concurrent GC threads (default: ParallelGCThreads / 4)
-XX:ConcGCThreads=4
Diagnosing G1 problems
Look in GC logs for:
[gc] GC(42) Pause Full (Ergonomics) 4096M->3584M(8192M) 8240.123ms
A Full GC means G1 could not recover enough space through concurrent marking + mixed GC cycles. Causes:
- Heap is too small
- Promotion failure (too many objects being promoted to old gen concurrently)
- Humongous object allocation rate is high
Fix: increase heap, increase G1HeapRegionSize for humongous objects, or reduce allocation rate.
Metaspace Sizing
Metaspace holds class metadata. It is unbounded by default — set an upper limit to prevent unbounded growth:
-XX:MetaspaceSize=256m # initial commit size (triggers GC when reached)
-XX:MaxMetaspaceSize=512m # hard upper limit
Metaspace growth is normally driven by class loading. If Metaspace grows continuously, check for:
- Dynamic proxy class generation (e.g., CGLib proxies that are not reused)
- Plugin systems that load many classloaders
- Memory leaks in application classloaders (common in OSGi, JBoss modules)
Container-Aware JVM Flags
Java 11’s -XX:+UseContainerSupport (enabled by default) makes the JVM read container resource limits from cgroups rather than the host:
# In a container with 4 CPUs and 4 GB RAM limits:
# Java 8: sees host CPUs/RAM (e.g., 64 CPUs, 256 GB)
# Java 11: sees container limits (4 CPUs, 4 GB) — correct behaviour
Container-aware heap sizing
# Java 11 sets max heap to 25% of container memory by default
# For a 4 GB container: 4096 * 0.25 = 1024 MB (too small for most apps)
# Override the ratio
-XX:MaxRAMPercentage=75.0 # use 75% of container memory for heap (3 GB of 4 GB)
-XX:InitialRAMPercentage=50.0 # initial heap = 50% of container memory
# Or set absolute sizes (preferred for predictability)
-Xms2g -Xmx3g
Docker resource limits
# docker-compose.yml
services:
myapp:
image: myapp:latest
mem_limit: 4g
cpus: 2
environment:
- JAVA_OPTS=-Xms2g -Xmx3g -XX:+UseContainerSupport -XX:ParallelGCThreads=2
Kubernetes resource requests and limits
resources:
requests:
memory: "2Gi"
cpu: "1"
limits:
memory: "4Gi"
cpu: "2"
The JVM reads the limits.memory value via cgroups. With -XX:MaxRAMPercentage=75.0, a 4 Gi limit gives a 3 Gi max heap.
AppCDS for Faster Startup
Application Class-Data Sharing pre-builds a shared archive of class metadata, reducing startup time by 20–40%:
# Step 1: Generate class list (run normally, capture loaded classes)
java -XX:DumpLoadedClassList=classes.lst -jar myapp.jar
# Let the application reach steady state, then stop it
# Step 2: Create the shared archive
java -Xshare:dump \
-XX:SharedClassListFile=classes.lst \
-XX:SharedArchiveFile=myapp.jsa \
-cp myapp.jar \
MainClass
# Step 3: Launch with the archive
java -Xshare:on \
-XX:SharedArchiveFile=myapp.jsa \
-jar myapp.jar
In Docker (build-time archive)
FROM eclipse-temurin:11-jdk AS appbuild
COPY target/myapp.jar /app/myapp.jar
WORKDIR /app
# Generate class list (use a quick-exit flag in your app)
RUN java -XX:DumpLoadedClassList=classes.lst -jar myapp.jar --exit-after-init
# Build AppCDS archive
RUN java -Xshare:dump \
-XX:SharedClassListFile=classes.lst \
-XX:SharedArchiveFile=myapp.jsa \
-jar myapp.jar --exit-after-init
FROM eclipse-temurin:11-jre
COPY --from=appbuild /app/myapp.jar /app/myapp.jar
COPY --from=appbuild /app/myapp.jsa /app/myapp.jsa
ENTRYPOINT ["java", \
"-Xshare:on", \
"-XX:SharedArchiveFile=/app/myapp.jsa", \
"-jar", "/app/myapp.jar"]
JFR Always-On Production Recording
java \
-XX:StartFlightRecording=disk=true,\
maxsize=100m,\
maxage=6h,\
settings=default,\
filename=/var/log/app/jfr/ \
-XX:FlightRecorderOptions=stackdepth=256 \
-jar myapp.jar
When an incident occurs:
# Dump the last 6 hours of JFR data
jcmd $(pgrep -f myapp.jar) JFR.dump filename=/tmp/incident-$(date +%Y%m%d-%H%M%S).jfr
Transfer the .jfr file and open it in JDK Mission Control for analysis.
Security Hardening
Disable weak TLS protocols and ciphers
java \
-Djdk.tls.disabledAlgorithms="SSLv3,TLSv1,TLSv1.1,RC4,DES,MD5withRSA,DH keySize < 1024,EC keySize < 224" \
-Djdk.certpath.disabledAlgorithms="MD2,MD5,SHA1 jdkCA & usage TLSServer,RSA keySize < 1024,DSA keySize < 1024,EC keySize < 224" \
-jar myapp.jar
Or edit $JAVA_HOME/conf/security/java.security permanently.
Prefer TLS 1.3
-Djdk.tls.client.protocols="TLSv1.3,TLSv1.2" # prefer 1.3, fall back to 1.2
Serialization filters (defence against deserialization attacks)
# Allowlist approach — only allow known safe classes to deserialize
-Djdk.serialFilter="com.example.**;java.lang.*;java.util.*;!*"
Or set programmatically:
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.example.*;java.lang.*;java.util.*;!*"
);
ObjectInputFilter.Config.setSerialFilter(filter);
Thread Pool and Connection Pool Sizing
HTTP thread pool (Spring Boot)
# application.properties
server.tomcat.threads.max=200 # max concurrent requests
server.tomcat.threads.min-spare=10 # always-warm threads
server.tomcat.accept-count=100 # queue depth before rejecting
server.tomcat.connection-timeout=5000
Database connection pool (HikariCP — Spring Boot default)
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.leak-detection-threshold=60000
Pool size formula: max connections = (core_count * 2) + effective_spindle_count
For a 4-core machine with SSD: (4 * 2) + 1 = 9. For cloud databases with network latency, a slightly higher value (15–25) is typical.
Monitoring Checklist
| Signal | Tool | Threshold |
|---|---|---|
| Heap usage | JFR, JMX, Micrometer | Alert if > 85% of -Xmx after GC |
| GC pause time | GC logs, JFR | Alert if any pause > 2× MaxGCPauseMillis |
| GC throughput | GC logs | Alert if GC takes > 5% of CPU time |
| Metaspace | JFR | Alert if > 80% of -XX:MaxMetaspaceSize |
| Thread count | JMX, JFR | Alert if growing unboundedly |
| File descriptors | OS metrics | Alert if > 80% of ulimit -n |
Complete Startup Script Example
#!/bin/bash
# start.sh — Java 11 production startup
APP_HOME=/opt/myapp
LOG_HOME=/var/log/myapp
mkdir -p $LOG_HOME
exec java \
# Heap
-Xms${HEAP_INITIAL:-2g} \
-Xmx${HEAP_MAX:-4g} \
\
# Metaspace
-XX:MetaspaceSize=256m \
-XX:MaxMetaspaceSize=512m \
\
# GC
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=${GC_PAUSE_TARGET:-200} \
-XX:G1HeapRegionSize=16m \
-XX:InitiatingHeapOccupancyPercent=45 \
\
# Container support
-XX:+UseContainerSupport \
-XX:MaxRAMPercentage=75.0 \
\
# AppCDS
-Xshare:on \
-XX:SharedArchiveFile=$APP_HOME/myapp.jsa \
\
# OOM handling
-XX:+ExitOnOutOfMemoryError \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=$LOG_HOME/heapdump.hprof \
\
# GC logging
-Xlog:gc*,gc+heap=debug,safepoint:file=$LOG_HOME/gc.log:time,uptime,pid:filecount=5,filesize=20m \
\
# JFR
-XX:StartFlightRecording=disk=true,maxsize=100m,maxage=6h,settings=default \
-XX:FlightRecorderOptions=stackdepth=256 \
\
# Security
-Djdk.tls.client.protocols="TLSv1.3,TLSv1.2" \
-Djdk.tls.disabledAlgorithms="SSLv3,TLSv1,TLSv1.1,RC4,DES,MD5withRSA" \
\
# Module access for frameworks
--add-opens java.base/java.lang=ALL-UNNAMED \
--add-opens java.base/java.util=ALL-UNNAMED \
\
-jar $APP_HOME/myapp.jar "$@"
Series Complete
You have reached the end of the Java 11 Tutorial series. You now have a complete understanding of every significant feature introduced across Java 9, 10, and 11 — from the Module System and var to TLS 1.3, ZGC, and production tuning.
Series articles:
- Java 11 Overview: The Road from Java 8 to LTS
- Setting Up Java 11
- Module System (JPMS)
- var Keyword
- New String Methods
- Collection Factory Methods
- Stream & Optional Enhancements
- Files and IO API
- HTTP Client API
- Tooling: JShell, jlink, Single-File Programs
- Garbage Collection: G1GC, ZGC, Epsilon, AppCDS
- Flight Recorder and JVM Monitoring
- Security: TLS 1.3, ChaCha20, Curve25519
- Removed and Deprecated APIs
- Migration Guide: Java 8 → Java 11
- Production Checklist and Performance Best Practices
Next steps: Java 17 Tutorial | Java 21 Tutorial