Java 17 Production Checklist and Performance Best Practices
Production Baseline JVM Flags
Start every Java 17 production deployment with this baseline:
java \
# GC — choose one (see GC section)
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
\
# Heap sizing
-Xms4g -Xmx4g \
\
# GC logging — essential for diagnosis
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=5,filesize=20m \
\
# OOM diagnostics
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/app/heap-dump.hprof \
-XX:+ExitOnOutOfMemoryError \
\
# Metaspace
-XX:MaxMetaspaceSize=512m \
\
# Code cache
-XX:ReservedCodeCacheSize=512m \
\
# JFR — always-on profiling
-XX:StartFlightRecording=duration=0,filename=/var/log/app/profile.jfr,maxsize=256m,maxage=24h \
\
# Container awareness
-XX:+UseContainerSupport \
-XX:MaxRAMPercentage=75.0 \
\
-jar app.jar
GC Selection for Java 17
G1GC (Default — General Purpose)
G1GC is the default since Java 9 and the right choice for most Java 17 applications:
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 # target max pause (200ms default)
-XX:G1HeapRegionSize=16m # only override if default is too small
-XX:+UseStringDeduplication # saves heap for string-heavy apps
G1GC works well for:
- Applications with 4GB+ heaps
- Mixed workloads (some latency sensitivity, some throughput)
- Applications with significant live data (large old generation)
ZGC (Low-Latency)
Java 17 ships with production-ready ZGC (non-generational):
-XX:+UseZGC
-XX:ConcGCThreads=4 # concurrent GC threads (default: CPU count / 8)
-XX:+ZUncommit # return unused heap to OS (default: enabled)
-XX:ZUncommitDelay=300 # seconds before uncommitting (default: 300)
ZGC is the right choice for:
- Services with strict latency requirements (P99 < 10ms)
- Large heaps (64GB+) where G1GC pauses become significant
- Applications with unpredictable allocation patterns
Note: Generational ZGC (the major performance improvement) requires Java 21. Java 17’s ZGC is non-generational — it works well but is not as efficient for young-object-heavy workloads as Java 21’s Generational ZGC.
Choosing GC
Application type → Recommended GC
─────────────────────────────────────────────
Web service (typical) → G1GC (default)
Batch processing → G1GC or ParallelGC
Low-latency service (<10ms) → ZGC
Large heap (>16GB) → ZGC
Microservice, small container → G1GC or Serial
Heap Sizing
The Equal Xms/Xmx Rule
Set min and max heap equal in production:
-Xms4g -Xmx4g
Equal sizes prevent heap resizing events (which cause GC pauses and delay startup) and make capacity predictable.
Container-Aware Sizing
In containers (Docker, Kubernetes), use percentage-based sizing:
-XX:+UseContainerSupport # reads cgroup limits (default on Java 11+)
-XX:MaxRAMPercentage=75.0 # 75% of container memory for heap
-XX:InitialRAMPercentage=50.0
Reserve 25% for:
- JVM non-heap (Metaspace, thread stacks, JIT code cache)
- OS page cache
- Native memory (NIO buffers, off-heap allocations)
Metaspace
Metaspace stores class metadata. It grows unboundedly by default. Cap it:
-XX:MetaspaceSize=128m # initial size before first GC trigger
-XX:MaxMetaspaceSize=512m # hard cap
A growing Metaspace (checked with jcmd <pid> VM.native_memory) suggests:
- Dynamic class generation (cglib, ByteBuddy in non-steady state)
- Class leaks from custom classloaders
Observability
Java Flight Recorder (JFR)
JFR is built-in, has <1% overhead, and is always-on safe:
-XX:StartFlightRecording=duration=0,filename=/var/log/app/profile.jfr,\
maxsize=256m,maxage=24h,settings=profile
On-demand dump:
jcmd <pid> JFR.dump filename=/tmp/incident.jfr
GC Log Parsing
Enable structured GC logs (Java 9+ unified logging):
-Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=20m
Analyze with:
- GCEasy (gcease.io) — web-based GC log analyzer
- GCViewer — open-source desktop tool
- JDK Mission Control (JMC) — analyzes JFR files with GC viewer
Key Metrics to Monitor
| Metric | Healthy | Warning |
|---|---|---|
| GC pause P99 (G1) | <200ms | >500ms |
| GC pause P99 (ZGC) | <10ms | >50ms |
| Time in GC | <5% | >10% |
| Heap used / Heap max | <80% | >90% |
| Metaspace used | Stable | Growing |
| Code cache used | <80% of ReservedCodeCacheSize | >90% |
| Thread count | Stable | Spikes |
jcmd Reference
jcmd -l # list JVMs
jcmd <pid> Thread.print # thread dump
jcmd <pid> GC.heap_info # heap summary
jcmd <pid> GC.class_histogram # objects by type
jcmd <pid> VM.flags # active JVM flags
jcmd <pid> VM.native_memory summary # native memory breakdown
jcmd <pid> JFR.start ... # start JFR
jcmd <pid> JFR.dump ... # dump JFR
jcmd <pid> GC.heap_dump /tmp/h.hprof # heap dump
JIT Compilation
Code Cache
The JIT stores compiled native code in the code cache. Default size (240m) is often too small for large apps:
-XX:ReservedCodeCacheSize=512m
-XX:+UseCodeCacheFlushing # allow old code to be evicted
Monitor:
jcmd <pid> VM.native_memory summary | grep CodeCache
If code cache fills, the JVM falls back to interpretation — causing sudden latency spikes.
Tiered Compilation
Tiered compilation (default) uses C1 for quick compilation and C2 for optimal compilation of hot methods. Do not disable it.
For faster startup at reduced peak throughput (batch jobs, lambdas):
-XX:TieredStopAtLevel=1 # only compile to C1 (fast start)
Container Deployment
Kubernetes Resource Configuration
resources:
requests:
cpu: "1"
memory: "2Gi"
limits:
cpu: "2"
memory: "2Gi"
With -XX:MaxRAMPercentage=75, Java automatically uses ~1.5Gi for heap with a 2Gi container limit.
CPU Throttling
Kubernetes CPU throttling (CFS scheduler) affects JIT compilation and GC threads. Set requests below limits to allow burst:
resources:
requests:
cpu: "500m" # guaranteed minimum
limits:
cpu: "2000m" # burst allowed (for JIT warmup, GC)
Liveness and Readiness Probes
For Spring Boot applications, configure probes to avoid premature traffic during JIT warmup:
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30 # wait for JIT warmup
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 20
periodSeconds: 5
Startup Optimization
Class Data Sharing (CDS)
CDS pre-parses JDK classes and stores them in a shared archive:
# Built-in CDS (already enabled by default in Java 17)
# Verify it's active:
java -Xshare:on -version
Application Class Data Sharing (AppCDS)
AppCDS extends CDS to include your application’s classes:
# Step 1: Record classes loaded at startup
java -XX:DumpLoadedClassList=classes.lst -jar app.jar
# Run a few warmup requests, then Ctrl+C
# Step 2: Create shared archive
java -Xshare:dump \
-XX:SharedArchiveFile=app-cds.jsa \
-XX:SharedClassListFile=classes.lst \
-jar app.jar
# Step 3: Run with shared archive
java -Xshare:on \
-XX:SharedArchiveFile=app-cds.jsa \
-jar app.jar
AppCDS reduces startup time by 20–40% and reduces JVM memory footprint.
Build AppCDS into Docker Image
FROM eclipse-temurin:17-jre as builder
COPY target/app.jar /app/app.jar
WORKDIR /app
RUN java -XX:DumpLoadedClassList=classes.lst -jar app.jar &
RUN sleep 15 && kill %1 || true
RUN java -Xshare:dump -XX:SharedArchiveFile=app-cds.jsa -XX:SharedClassListFile=classes.lst -jar app.jar || true
FROM eclipse-temurin:17-jre
COPY --from=builder /app/app.jar /app/app.jar
COPY --from=builder /app/app-cds.jsa /app/app-cds.jsa
ENTRYPOINT ["java", "-Xshare:on", "-XX:SharedArchiveFile=/app/app-cds.jsa", "-jar", "/app/app.jar"]
Security Hardening
# Disable remote debugging in production
# (remove any -agentlib:jdwp=... flags)
# TLS configuration
-Dhttps.protocols=TLSv1.2,TLSv1.3
-Djdk.tls.client.protocols=TLSv1.2,TLSv1.3
-Djdk.tls.disabledAlgorithms=SSLv3,TLSv1,TLSv1.1,RC4,DES,MD5withRSA
# Deserialization protection (Article 10)
-Djdk.serialFilter="com.example.**;java.lang.*;java.util.*;maxrefs=500;maxdepth=8;!*"
# JNDI hardening (Log4Shell defense)
-Dcom.sun.jndi.ldap.object.trustURLCodebase=false
-Dcom.sun.jndi.rmi.object.trustURLCodebase=false
Memory Profiling
Heap Dump Analysis
# Trigger without crashing
jcmd <pid> GC.heap_dump /tmp/heap.hprof
# Analyze with MAT (Eclipse Memory Analyzer Tool)
# or VisualVM, or JDK Mission Control
Native Memory Tracking
# Enable at JVM start
-XX:NativeMemoryTracking=summary
# Check at runtime
jcmd <pid> VM.native_memory summary
# Baseline + diff
jcmd <pid> VM.native_memory baseline
# ... time passes ...
jcmd <pid> VM.native_memory summary.diff
Unexpected native memory growth (beyond stable heap) indicates:
- Too many threads (stack space)
- JNI or FFM API memory leaks
- Code cache growth
Production Checklist
GC and Heap
[ ] Select GC: G1GC (general) or ZGC (low-latency)
[ ] Set -Xms = -Xmx (or use MaxRAMPercentage in containers)
[ ] Set -XX:MaxMetaspaceSize
[ ] Set -XX:ReservedCodeCacheSize=512m
[ ] Configure GC logging to rotating files
[ ] Set -XX:+HeapDumpOnOutOfMemoryError and -XX:+ExitOnOutOfMemoryError
Observability
[ ] Enable always-on JFR with circular buffer (maxsize, maxage)
[ ] Export JMX metrics to monitoring stack
[ ] Alert on: heap >80%, GC pause P99 >threshold, code cache >80%
[ ] Set up GC log parsing and dashboards
Container
[ ] Use -XX:+UseContainerSupport (default in Java 11+)
[ ] Use -XX:MaxRAMPercentage=75 instead of -Xmx
[ ] Set CPU requests and limits in Kubernetes
[ ] Configure liveness/readiness probes with adequate initialDelaySeconds
Startup
[ ] Build AppCDS archive into Docker image
[ ] Verify CDS is active with -Xshare:on
Security
[ ] Remove remote debug agent from production flags
[ ] Set TLS minimum to 1.2
[ ] Set deserialization filter (-Djdk.serialFilter)
[ ] Disable JNDI URL codebase loading
Migration-Specific
[ ] Remove all --illegal-access flags (invalid in Java 17)
[ ] Audit remaining --add-opens; track as debt to eliminate via library upgrades
[ ] Remove -XX:MaxPermSize
[ ] Remove -XX:+UseConcMarkSweepGC
Series Complete
You have finished the Java 17 Tutorial series. Here is what was covered:
- Java 17 Overview
- Setting Up Java 17
- Text Blocks (JEP 378)
- Records (JEP 395)
- Pattern Matching for instanceof (JEP 394)
- Switch Expressions (JEP 361)
- Sealed Classes (JEP 409)
- Pattern Matching for switch (JEP 406, Preview)
- Enhanced PRNGs (JEP 356)
- Deserialization Filters (JEP 415)
- JDK Encapsulation and Removed APIs
- Foreign Function & Memory API (JEP 412)
- Migration Guide
- Production Best Practices
Next Steps
Ready for the next LTS? The Java 21 Tutorial Series covers Virtual Threads, Pattern Matching (Final), Record Patterns, Sequenced Collections, Generational ZGC, and everything else that arrived between Java 17 and Java 21.