Avancé
⭐ Article vedette

Tuning JVM en Production : Optimisation Avancée et Garbage Collection

Guide complet d'optimisation JVM pour la production : GC tuning, memory management, monitoring et configurations par workload.

Publié le
12 novembre 2024
Lecture
16 min
Vues
1.8k
Auteur
Florian Courouge
Java
JVM
Performance
Garbage Collection
Production
Tuning
Memory

Table des matières

📋 Vue d'ensemble rapide des sujets traités dans cet article

Cliquez sur les sections ci-dessous pour naviguer rapidement

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.

JVM Architecture Overview

💡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

JVM Memory Layout

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

Garbage Collectors Comparison

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

JVM Memory Sizing

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

JIT Compilation Process

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

JVM Monitoring Dashboard

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

Production

Maintenance

💡Conclusion

L'optimisation JVM en production est un processus itératif qui combine :

Analyse et mesure :

Configuration adaptée :

Surveillance proactive :

Évolution continue :

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.

À propos de l'auteur

Florian Courouge - Expert DevOps et Apache Kafka avec plus de 5 ans d'expérience dans l'architecture de systèmes distribués et l'automatisation d'infrastructures.

Cet article vous a été utile ?

Découvrez mes autres articles techniques ou contactez-moi pour discuter de vos projets DevOps et Kafka.