Part 16 of 16

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

SymptomLikely CauseAction
Frequent young GC (every <1s)Eden too smallIncrease heap
Long Full GC pausesHeap too small, or too much long-lived dataIncrease heap, check for memory leaks
OutOfMemoryError: Java heap spaceMax heap exhaustedIncrease -Xmx, fix memory leak
OutOfMemoryError: MetaspaceToo many classloaders or dynamic proxy classesIncrease -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

SignalToolThreshold
Heap usageJFR, JMX, MicrometerAlert if > 85% of -Xmx after GC
GC pause timeGC logs, JFRAlert if any pause > 2× MaxGCPauseMillis
GC throughputGC logsAlert if GC takes > 5% of CPU time
MetaspaceJFRAlert if > 80% of -XX:MaxMetaspaceSize
Thread countJMX, JFRAlert if growing unboundedly
File descriptorsOS metricsAlert 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:

  1. Java 11 Overview: The Road from Java 8 to LTS
  2. Setting Up Java 11
  3. Module System (JPMS)
  4. var Keyword
  5. New String Methods
  6. Collection Factory Methods
  7. Stream & Optional Enhancements
  8. Files and IO API
  9. HTTP Client API
  10. Tooling: JShell, jlink, Single-File Programs
  11. Garbage Collection: G1GC, ZGC, Epsilon, AppCDS
  12. Flight Recorder and JVM Monitoring
  13. Security: TLS 1.3, ChaCha20, Curve25519
  14. Removed and Deprecated APIs
  15. Migration Guide: Java 8 → Java 11
  16. Production Checklist and Performance Best Practices

Next steps: Java 17 Tutorial | Java 21 Tutorial