Tuning JVM en Production : Optimisation Avancée et Garbage Collection
L'optimisation de la JVM en production est un art qui combine théorie et expérience pratique. Entre choix du garbage collector, dimensionnement de la heap et monitoring des métriques critiques, ce guide vous accompagne pour maximiser les performances de vos applications Java.
Les fondamentaux du tuning JVM
Architecture mémoire JVM
La JVM organise la mémoire en plusieurs zones distinctes :
Heap Memory :
- Young Generation : Eden + Survivor spaces (S0, S1)
- Old Generation : objets à longue durée de vie
- Metaspace : métadonnées des classes (remplace PermGen depuis Java 8)
Non-Heap Memory :
- Direct Memory : ByteBuffers hors heap
- Code Cache : code natif compilé par le JIT
- Stack Memory : pile d'exécution par thread
Métriques critiques à surveiller
# Monitoring de base
jstat -gc <pid> 250ms # GC stats toutes les 250ms
jstat -gccapacity <pid> # Capacités des générations
jstat -gcutil <pid> 1s # Utilisation en pourcentage
# Métriques clés
# - GC Frequency : fréquence des collections
# - GC Duration : temps de pause
# - Allocation Rate : taux d'allocation mémoire
# - Promotion Rate : taux de promotion vers Old Gen
Choix et configuration des Garbage Collectors

G1GC (Garbage First) - Recommandé pour la plupart des cas
Quand utiliser G1GC :
- Applications avec heap > 6GB
- Latence ciblée < 100ms
- Workloads mixtes (allocation + rétention)
# Configuration G1GC optimisée
-XX:+UseG1GC
-XX:MaxGCPauseTimeMillis=50 # Cible de latence aggressive
-XX:G1HeapRegionSize=16m # Taille des régions
-XX:G1NewSizePercent=20 # 20% de heap pour Young Gen
-XX:G1MaxNewSizePercent=30 # Maximum 30% pour Young Gen
-XX:G1MixedGCCountTarget=8 # Nombre de Mixed GC cycles
-XX:G1MixedGCLiveThresholdPercent=85 # Seuil pour Mixed GC
-XX:G1ReservePercent=10 # Réserve pour éviter Full GC
# Tuning avancé G1
-XX:G1ConcRefinementThreads=4 # Threads de raffinement concurrent
-XX:G1ParallelGCThreads=8 # Threads parallèles pour GC
-XX:ConcGCThreads=2 # Threads concurrent marking
ZGC - Pour ultra-faible latence (Java 15+)
Quand utiliser ZGC :
- Latence critique < 10ms
- Heap très large (>100GB)
- Applications temps réel
# Configuration ZGC
-XX:+UseZGC
-XX:+UnlockExperimentalVMOptions # Nécessaire jusqu'à Java 15
-XX:SoftMaxHeapSize=30g # Soft limit pour déclenchement GC
-XX:ZCollectionInterval=5 # Intervalle minimum entre GC (secondes)
-XX:ZUncommitDelay=300 # Délai avant libération mémoire OS
# Monitoring ZGC spécifique
-XX:+LogVMOutput
-XX:LogFile=gc.log
-XX:+UseTransparentHugePages # Performances mémoire
Shenandoah - Alternative low-latency
# Configuration Shenandoah
-XX:+UseShenandoahGC
-XX:ShenandoahGCHeuristics=adaptive # Heuristique adaptative
-XX:ShenandoahMinFreeThreshold=10 # Seuil minimum mémoire libre
-XX:ShenandoahAllocationThreshold=2 # Seuil déclenchement allocation GC
Parallel GC - Pour throughput maximum
# Configuration Parallel GC (batch processing)
-XX:+UseParallelGC
-XX:ParallelGCThreads=8 # Threads parallèles
-XX:MaxGCPauseTimeMillis=200 # Pause acceptable plus élevée
-XX:GCTimeRatio=99 # 1% temps dans GC max
Dimensionnement et allocation mémoire

Calcul de la heap size
# Règles de dimensionnement
# Heap = (Working Set + Allocation Buffer) * Safety Factor
# Working Set = données résidentes en mémoire
# Allocation Buffer = taux allocation * latence GC acceptable
# Safety Factor = 1.5 à 2.0 selon la criticité
# Exemple pour application web
-Xms4g -Xmx4g # Heap fixe 4GB
-XX:NewRatio=3 # Old:Young = 3:1
-XX:SurvivorRatio=8 # Eden:Survivor = 8:1
# Exemple pour microservice
-Xms512m -Xmx1g # Heap variable
-XX:InitialRAMPercentage=50 # 50% RAM container au démarrage
-XX:MaxRAMPercentage=80 # 80% RAM container maximum
Configuration Metaspace et autres zones
# Metaspace (métadonnées classes)
-XX:MetaspaceSize=256m # Taille initiale
-XX:MaxMetaspaceSize=512m # Limite maximale
-XX:CompressedClassSpaceSize=128m # Espace compressed class pointers
# Code Cache (JIT compilation)
-XX:InitialCodeCacheSize=64m # Taille initiale
-XX:CodeCacheExpansionSize=32m # Expansion par bloc
-XX:ReservedCodeCacheSize=256m # Taille maximale
# Direct Memory (NIO, netty)
-XX:MaxDirectMemorySize=1g # Limite mémoire directe
Optimisations JIT Compiler

Configuration avancée du compilateur
# Optimisations JIT
-XX:+TieredCompilation # Compilation à plusieurs niveaux
-XX:TieredStopAtLevel=4 # Niveau maximum (C2 compiler)
-XX:CompileThreshold=10000 # Seuil compilation standard methods
-XX:OnStackReplacePercentage=933 # Seuil OSR (On Stack Replacement)
# Optimisations agressives
-XX:+UseStringDeduplication # Déduplication des chaînes (G1GC)
-XX:+UseCompressedOops # Pointeurs compressés (<32GB heap)
-XX:+UseCompressedClassPointers # Pointeurs classe compressés
-XX:+OptimizeStringConcat # Optimisation concaténation
Compilation Ahead-of-Time (AOT)
# Génération profile JIT (Java 9+)
-XX:+FlightRecorder
-XX:StartFlightRecording=duration=60s,filename=profile.jfr
Génération profile JIT (Java 9+)
-XX:+FlightRecorder -XX:StartFlightRecording=duration=60s,filename=profile.jfr
Utilisation du profil pour optimiser
jaotc --output libHelloWorld.so HelloWorld.class -XX:AOTLibrary=./libHelloWorld.so
## Configurations par type d'application
### Applications web (Spring Boot, Tomcat)
```bash
# Configuration optimisée pour applications web
-Xms2g -Xmx2g # Heap fixe pour éviter les expansions
-XX:+UseG1GC # G1GC pour latence prévisible
-XX:MaxGCPauseTimeMillis=100 # Latence cible 100ms
-XX:G1HeapRegionSize=16m # Régions adaptées à la heap
-XX:G1NewSizePercent=30 # 30% pour Young Generation
-XX:G1MaxNewSizePercent=40 # Maximum 40% pour Young Gen
-XX:G1MixedGCCountTarget=8 # Cycles Mixed GC
-XX:G1MixedGCLiveThresholdPercent=85 # Seuil Mixed GC
# Optimisations spécifiques web
-XX:+UseStringDeduplication # Déduplication des chaînes
-XX:+OptimizeStringConcat # Optimisation StringBuilder
-XX:+UseCompressedOops # Pointeurs compressés
-XX:+UseCompressedClassPointers # Pointeurs classe compressés
# Tuning des threads
-XX:ConcGCThreads=2 # Threads GC concurrent
-XX:ParallelGCThreads=8 # Threads GC parallèle
-Djava.awt.headless=true # Mode headless pour serveurs
# Monitoring et debugging
-XX:+PrintGC # Logs GC basiques
-XX:+PrintGCDetails # Logs GC détaillés
-XX:+PrintGCTimeStamps # Timestamps dans logs
-XX:+PrintGCApplicationStoppedTime # Temps d'arrêt application
-Xloggc:/var/log/app/gc.log # Fichier de log GC
-XX:+UseGCLogFileRotation # Rotation des logs
-XX:NumberOfGCLogFiles=5 # Nombre de fichiers de log
-XX:GCLogFileSize=100M # Taille max par fichier
Microservices (conteneurs, Kubernetes)
# Configuration pour microservices en conteneurs
-XX:+UseContainerSupport # Support natif conteneurs (Java 10+)
-XX:InitialRAMPercentage=50 # 50% RAM container au démarrage
-XX:MaxRAMPercentage=80 # 80% RAM container maximum
-XX:MinRAMPercentage=50 # Minimum 50% RAM
# G1GC adapté aux petites heaps
-XX:+UseG1GC
-XX:MaxGCPauseTimeMillis=50 # Latence très faible
-XX:G1HeapRegionSize=8m # Régions plus petites
-XX:G1NewSizePercent=40 # Plus de Young Gen
-XX:G1MaxNewSizePercent=50
# Optimisations startup
-XX:+TieredCompilation # Compilation à niveaux
-XX:TieredStopAtLevel=1 # Compilation rapide au démarrage
-XX:+UseAppCDS # Class Data Sharing
-Xshare:on # Partage des classes
# Réduction de l'empreinte mémoire
-XX:+UseCompressedOops
-XX:+UseCompressedClassPointers
-XX:CompressedClassSpaceSize=64m # Espace classe réduit
-XX:MetaspaceSize=128m # Metaspace initial
-XX:MaxMetaspaceSize=256m # Limite Metaspace
Applications batch (traitement de données)
# Configuration pour traitement batch haute performance
-Xms8g -Xmx8g # Heap importante et fixe
-XX:+UseParallelGC # Parallel GC pour throughput
-XX:ParallelGCThreads=16 # Threads parallèles
-XX:MaxGCPauseTimeMillis=500 # Pause acceptable plus élevée
-XX:GCTimeRatio=99 # 1% temps dans GC maximum
# Optimisations pour gros volumes
-XX:NewRatio=2 # Old:Young = 2:1
-XX:SurvivorRatio=8 # Eden:Survivor = 8:1
-XX:TargetSurvivorRatio=90 # 90% utilisation Survivor
-XX:MaxTenuringThreshold=15 # Promotion après 15 cycles
# Large Pages pour performance
-XX:+UseLargePages # Pages mémoire larges
-XX:LargePageSizeInBytes=2m # Taille des large pages
# Optimisations I/O et allocation
-XX:+UseNUMA # Support NUMA
-XX:+AlwaysPreTouch # Pré-allocation mémoire
-XX:-UseBiasedLocking # Désactiver biased locking
Applications haute fréquence (trading, gaming)
# Configuration ultra-faible latence
-Xms16g -Xmx16g # Heap importante et fixe
-XX:+UseZGC # ZGC pour latence < 10ms
-XX:+UnlockExperimentalVMOptions # Nécessaire pour ZGC
-XX:SoftMaxHeapSize=14g # Soft limit pour ZGC
# Élimination des sources de latence
-XX:+AlwaysPreTouch # Pré-allocation complète
-XX:+UseLargePages # Large pages obligatoire
-XX:+UseTransparentHugePages # Huge pages transparentes
-XX:-UseBiasedLocking # Pas de biased locking
-XX:+DisableExplicitGC # Pas de System.gc()
# Optimisations JIT agressives
-XX:+TieredCompilation
-XX:TieredStopAtLevel=4 # Compilation C2 complète
-XX:CompileThreshold=1000 # Compilation précoce
-XX:OnStackReplacePercentage=140 # OSR agressif
# Monitoring minimal (overhead réduit)
-XX:+FlightRecorder # JFR pour profiling
-XX:StartFlightRecording=duration=0,filename=app.jfr
Monitoring et observabilité JVM

Métriques essentielles à surveiller
# Script de monitoring JVM complet
#!/bin/bash
# jvm-monitor.sh
PID=$1
if [[ -z "$PID" ]]; then
echo "Usage: $0 <java-pid>"
exit 1
fi
echo "=== JVM Monitoring for PID $PID ==="
echo "Timestamp: $(date)"
echo
# Informations générales JVM
echo "--- JVM Info ---"
jinfo $PID | grep -E "(java.version|java.vm.name|java.vm.version)"
echo
# Utilisation mémoire détaillée
echo "--- Memory Usage ---"
jstat -gc $PID | awk '
NR==1 {print "S0C\tS1C\tS0U\tS1U\tEC\tEU\tOC\tOU\tMC\tMU\tCCSC\tCCSU\tYGC\tYGCT\tFGC\tFGCT\tGCT"}
NR==2 {
printf "%.1f\t%.1f\t%.1f\t%.1f\t%.1f\t%.1f\t%.1f\t%.1f\t%.1f\t%.1f\t%.1f\t%.1f\t%d\t%.3f\t%d\t%.3f\t%.3f\n",
$1/1024, $2/1024, $3/1024, $4/1024, $5/1024, $6/1024, $7/1024, $8/1024,
$9/1024, $10/1024, $11/1024, $12/1024, $13, $14, $15, $16, $17
}'
echo
# Calculs de performance
echo "--- Performance Metrics ---"
jstat -gc $PID | awk '
NR==2 {
heap_used = ($3 + $4 + $6 + $8) / 1024
heap_total = ($1 + $2 + $5 + $7) / 1024
heap_util = (heap_used / heap_total) * 100
young_used = ($3 + $4 + $6) / 1024
young_total = ($1 + $2 + $5) / 1024
young_util = (young_used / young_total) * 100
old_util = ($8 / $7) * 100
meta_util = ($10 / $9) * 100
avg_ygc_time = $14 / $13
avg_fgc_time = $16 / $15
printf "Heap Usage: %.1f MB / %.1f MB (%.1f%%)\n", heap_used, heap_total, heap_util
printf "Young Gen: %.1f MB / %.1f MB (%.1f%%)\n", young_used, young_total, young_util
printf "Old Gen: %.1f%%\n", old_util
printf "Metaspace: %.1f%%\n", meta_util
printf "Avg YGC Time: %.3f ms\n", avg_ygc_time * 1000
printf "Avg FGC Time: %.3f ms\n", avg_fgc_time * 1000
}'
echo
# Threads et CPU
echo "--- Thread Info ---"
jstack $PID | grep -E "^\".*\" #[0-9]+" | wc -l | xargs echo "Active Threads:"
top -p $PID -n 1 | tail -1 | awk '{printf "CPU Usage: %s%%\nMemory: %s\n", $9, $10}'
echo
# Allocation rate (nécessite 2 mesures)
echo "--- Allocation Rate ---"
BEFORE=$(jstat -gc $PID | tail -1 | awk '{print $6 + $8}')
sleep 1
AFTER=$(jstat -gc $PID | tail -1 | awk '{print $6 + $8}')
RATE=$(echo "scale=2; ($AFTER - $BEFORE) / 1024" | bc)
echo "Allocation Rate: ${RATE} MB/s"
Alertes Prometheus pour JVM
# PrometheusRule pour monitoring JVM
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: jvm-alerts
namespace: monitoring
spec:
groups:
- name: jvm.rules
rules:
- alert: JVMMemoryUsageHigh
expr: jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"} > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "JVM heap memory usage is high"
description: "JVM heap memory usage is {{ $value | humanizePercentage }} on {{ $labels.instance }}"
- alert: JVMGCTimeHigh
expr: rate(jvm_gc_collection_seconds_sum[5m]) > 0.05
for: 2m
labels:
severity: warning
annotations:
summary: "JVM GC time is high"
description: "JVM is spending {{ $value | humanizePercentage }} time in GC on {{ $labels.instance }}"
- alert: JVMOldGenUsageHigh
expr: jvm_memory_used_bytes{area="heap",generation="old"} / jvm_memory_max_bytes{area="heap",generation="old"} > 0.9
for: 3m
labels:
severity: critical
annotations:
summary: "JVM Old Generation usage is critical"
description: "Old Generation usage is {{ $value | humanizePercentage }} on {{ $labels.instance }}"
- alert: JVMMetaspaceUsageHigh
expr: jvm_memory_used_bytes{area="nonheap",id="Metaspace"} / jvm_memory_max_bytes{area="nonheap",id="Metaspace"} > 0.85
for: 5m
labels:
severity: warning
annotations:
summary: "JVM Metaspace usage is high"
description: "Metaspace usage is {{ $value | humanizePercentage }} on {{ $labels.instance }}"
Profiling et analyse des performances
Utilisation de JProfiler en production
# Configuration JProfiler pour profiling en production
-agentpath:/opt/jprofiler/bin/linux-x64/libjprofilerti.so=port=8849,nowait
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/app/heapdumps/
-XX:OnOutOfMemoryError="kill -9 %p"
Analyse avec Java Flight Recorder (JFR)
# Démarrage avec JFR
-XX:+FlightRecorder
-XX:StartFlightRecording=duration=300s,filename=app-profile.jfr,settings=profile
# Analyse post-mortem
jfr print --events jdk.GarbageCollection app-profile.jfr
jfr print --events jdk.AllocationRequiringGC app-profile.jfr
jfr print --events jdk.CPULoad app-profile.jfr
# Conversion pour analyse externe
jfr print --json app-profile.jfr > app-profile.json
Heap dump analysis
# Génération heap dump
jcmd <pid> GC.run_finalization
jcmd <pid> VM.gc
jmap -dump:format=b,file=heap.hprof <pid>
# Analyse avec Eclipse MAT
mat -consoleLog -application org.eclipse.mat.api.parse heap.hprof \
-command=org.eclipse.mat.api.query \
-query="SELECT * FROM INSTANCEOF java.lang.String"
Troubleshooting des problèmes courants
OutOfMemoryError : Java heap space
# Diagnostic
echo "=== Heap Space Analysis ==="
jstat -gccapacity <pid>
jmap -histo <pid> | head -20
# Solutions
# 1. Augmenter la heap
-Xmx4g # au lieu de 2g
# 2. Optimiser le GC
-XX:+UseG1GC
-XX:G1HeapRegionSize=16m
# 3. Analyser les fuites mémoire
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heapdump.hprof
OutOfMemoryError : Metaspace
# Diagnostic Metaspace
jstat -gc <pid>
jcmd <pid> VM.metaspace
# Solutions
-XX:MetaspaceSize=256m # Taille initiale
-XX:MaxMetaspaceSize=512m # Limite maximale
-XX:CompressedClassSpaceSize=128m
# Monitoring des classes
jcmd <pid> VM.classloader_stats
GC overhead limit exceeded
# Diagnostic
jstat -gcutil <pid> 1s
# Solutions
# 1. Augmenter la heap
-Xmx8g
# 2. Changer de GC
-XX:+UseG1GC
-XX:MaxGCPauseTimeMillis=200
# 3. Optimiser l'allocation
-XX:NewRatio=1 # Plus de Young Generation
High CPU usage par la JVM
# Diagnostic threads
jstack <pid> > threads.dump
top -H -p <pid> # Threads par CPU
# Analyse des hot spots
perf record -p <pid> -g -- sleep 30
perf report
# Solutions JIT
-XX:+PrintCompilation
-XX:+UnlockDiagnosticVMOptions
-XX:+PrintInlining
Optimisations avancées par environnement
Conteneurs Docker/Kubernetes
# Dockerfile optimisé pour JVM
FROM openjdk:17-jre-slim
# Configuration JVM pour conteneurs
ENV JAVA_OPTS="-XX:+UseContainerSupport \
-XX:MaxRAMPercentage=75 \
-XX:+UseG1GC \
-XX:MaxGCPauseTimeMillis=100 \
-XX:+UseStringDeduplication \
-Djava.security.egd=file:/dev/./urandom"
# Optimisations startup
ENV JAVA_OPTS="$JAVA_OPTS \
-XX:+TieredCompilation \
-XX:TieredStopAtLevel=1 \
-Xshare:on"
COPY app.jar /app.jar
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app.jar"]
Configuration Kubernetes
apiVersion: apps/v1
kind: Deployment
metadata:
name: java-app
spec:
template:
spec:
containers:
- name: app
image: myapp:latest
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "1000m"
env:
- name: JAVA_OPTS
value: >-
-XX:+UseContainerSupport
-XX:InitialRAMPercentage=50
-XX:MaxRAMPercentage=75
-XX:+UseG1GC
-XX:MaxGCPauseTimeMillis=100
-XX:+ExitOnOutOfMemoryError
livenessProbe:
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 60
periodSeconds: 30
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
Cloud (AWS, GCP, Azure)
# Configuration pour instances cloud
# AWS EC2 avec instance store
-XX:+UseLargePages
-XX:+UseTransparentHugePages
-XX:+AlwaysPreTouch
# GCP avec SSD persistant
-XX:+UseG1GC
-XX:G1HeapRegionSize=32m
-XX:MaxGCPauseTimeMillis=50
# Azure avec Premium SSD
-XX:+UseZGC # Java 17+
-XX:SoftMaxHeapSize=30g
Checklist d'optimisation JVM
Pré-production
- [ ] Profiling complet de l'application (CPU, mémoire, I/O)
- [ ] Choix du GC adapté au workload et SLA
- [ ] Dimensionnement heap basé sur les métriques réelles
- [ ] Configuration des paramètres JIT selon les hot spots
- [ ] Tests de charge avec monitoring JVM
- [ ] Validation des temps de pause GC
- [ ] Configuration du monitoring et alerting
Production
- [ ] Monitoring continu des métriques JVM
- [ ] Alerting sur seuils critiques (heap, GC time, threads)
- [ ] Logs GC activés et analysés régulièrement
- [ ] Heap dumps automatiques sur OOM
- [ ] Profiling périodique pour détecter les régressions
- [ ] Tuning itératif basé sur les métriques
- [ ] Documentation des configurations et changements
Maintenance
- [ ] Mise à jour JVM régulière (patches de sécurité)
- [ ] Review des paramètres après montée de version
- [ ] Analyse des tendances de performance
- [ ] Optimisation continue basée sur l'évolution du code
- [ ] Formation équipe sur les bonnes pratiques JVM
- [ ] Veille technologique sur les nouvelles optimisations
Conclusion
L'optimisation JVM en production est un processus itératif qui combine :
Analyse et mesure :
- Profiling régulier pour identifier les goulots d'étranglement
- Monitoring continu des métriques critiques
- Tests de charge pour valider les optimisations
Configuration adaptée :
- Choix du GC selon les contraintes de latence et throughput
- Dimensionnement précis de la heap et des générations
- Paramètres JIT optimisés pour les hot paths
Surveillance proactive :
- Alerting sur les seuils critiques
- Logs GC analysés automatiquement
- Réaction rapide aux anomalies
Évolution continue :
- Tuning basé sur les métriques réelles
- Adaptation aux évolutions du code et de la charge
- Veille sur les nouvelles optimisations JVM
Avec ces pratiques et configurations, vous disposez d'une base solide pour optimiser vos applications Java en production et maintenir des performances élevées dans la durée.
Pour une formation approfondie sur le tuning JVM et l'analyse de performance, consultez mes sessions spécialisées en optimisation Java.