MFormations
Modern Java Engineering

Chapitre 11

11 - Performance Java

11 - Performance Java

Cours 11 : Performance Java

1. JVM Internals

1.1 Architecture de la JVM

┌─────────────────────────────────────┐
│         Class Loader Subsystem      │
├─────────────────────────────────────┤
│         Runtime Data Areas          │
│  ┌─────────┐ ┌───────────────────┐  │
│  │  Heap   │ │ Method Area       │  │
│  │         │ │ (Metaspace)       │  │
│  └─────────┘ └───────────────────┘  │
│  ┌─────────┐ ┌───────────────────┐  │
│  │  Stack  │ │ PC Registers      │  │
│  │  (N)    │ │ Native Stack      │  │
│  └─────────┘ └───────────────────┘  │
├─────────────────────────────────────┤
│      Execution Engine               │
│  ┌─────────┐ ┌───────────────────┐  │
│  │  JIT    │ │ Garbage Collector │  │
│  │ C1/C2   │ │ (G1, ZGC, ...)   │  │
│  └─────────┘ └───────────────────┘  │
└─────────────────────────────────────┘

1.2 Class Loading

  • Loading : Chargement des .class en mémoire
  • Linking : Vérification, préparation, résolution
  • Initialization : Exécution des static initializers
// Visualiser le class loading avec -XX:+TraceClassLoading
// Exemple : java -XX:+TraceClassLoading -jar app.jar

1.3 Class Loaders (Hiérarchie)

Bootstrap ClassLoader (rt.jar / java.base)
    └── Platform ClassLoader (modules JDK)
        └── Application ClassLoader (classpath)

1.4 JIT Compilation

La JVM utilise deux compilateurs JIT :

  • C1 (Client) : Compilation rapide, optimisations légères
  • C2 (Server) : Compilation lente, optimisations agressives
  • Tiered Compilation (par défaut depuis Java 8) : Démarre avec C1, passe à C2
# Flags JIT importants
-XX:+PrintCompilation    # Voir ce qui est compilé
-XX:TieredStopAtLevel=1  # Forcer C1 uniquement
-XX:-TieredCompilation   # Désactiver la compilation tiered
-XX:CompileThreshold=10000 # Seuil de compilation C1 (défaut: 1500)

1.5 Inlining

L'inlining est l'optimisation JIT la plus importante.

// Avant inlining
int result = add(a, b);  // Appel de méthode

// Après inlining
int result = a + b;      // Code inline

1.6 Escape Analysis

Permet d'allouer des objets sur la stack plutôt que sur le heap.

public int sum() {
    Point p = new Point(1, 2); // Peut être alloué sur la stack
    return p.x + p.y;
}

2. Garbage Collection

2.1 Générations de la mémoire heap

Heap
├── Young Generation
│   ├── Eden
│   └── Survivor Spaces (S0, S1)
├── Old Generation
└── Metaspace (hors heap, classes)

2.2 G1 GC (Garbage First) - Par défaut depuis Java 9

  • Régions de taille fixe (1-32 MB)
  • Priorise les régions avec le plus de déchets
  • Pause cible paramétrable
# Flags G1 courants
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200       # Pause cible
-XX:G1HeapRegionSize=4m        # Taille des régions
-XX:G1NewSizePercent=5         # Taille initiale Young
-XX:G1MaxNewSizePercent=60     # Taille max Young
-XX:InitiatingHeapOccupancyPercent=45 # Seuil déclenchement cycle concurrent
-XX:G1ReservePercent=10        # Réservation pour l'humain

2.3 ZGC (Depuis Java 15)

  • GC à très faible latence (< 1ms)
  • Gère des heaps jusqu'à 16 TB
  • Colored pointers (bits de pointeur)
  • Load barriers (barrières de chargement)
# Activer ZGC
-XX:+UseZGC
-XX:ZCollectionInterval=5      # Intervalle entre collections
-XX:ZAllocationSpikeTolerance=2.0 # Tolérance aux spikes

2.4 Shenandoah (Depuis Java 12)

  • GC à faible pause
  • Utilise des Brooks pointers
  • Compactage concurrent
# Activer Shenandoah
-XX:+UseShenandoahGC
-XX:ShenandoahGCHeuristics=adaptive
-XX:ShenandoahUncommitDelay=300000

2.5 Analyse des logs GC

# Activer les logs GC détaillés
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:gc.log
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=10
-XX:GCLogFileSize=10M

# Java 17+ (unified logging)
-Xlog:gc*:file=gc.log:time,uptime,level,tags
-Xlog:gc+ergo*=debug:file=gc-ergo.log

3. JFR (Flight Recorder)

3.1 Activation

# Démarrer avec JFR
java -XX:StartFlightRecording=name=profile,duration=120s,filename=recording.jfr \
     -jar app.jar

# Attacher JFR à un processus en cours
jcmd <pid> JFR.start name=profile duration=60s filename=recording.jfr
jcmd <pid> JFR.dump name=profile filename=recording.jfr
jcmd <pid> JFR.stop name=profile

3.2 Événements importants

// Événements JFR programmatiques
public class MyService {
    private static final JFR jfr = JFR.getInstance();
    
    public void processOrder() {
        Duration duration = jfr.duration("OrderProcessing", () -> {
            // Code à mesurer
        });
        System.out.println("Temps: " + duration);
    }
}

3.3 Configuration personnalisée

<!-- profile.jfc -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration version="2.0">
    <event name="jdk.ObjectAllocationInNewTLAB">
        <setting name="enabled">true</setting>
        <setting name="threshold">1 MB</setting>
    </event>
    <event name="jdk.GarbageCollection">
        <setting name="enabled">true</setting>
    </event>
</configuration>

4. Async Profiler

4.1 Installation et utilisation

# Profiler CPU
./profiler.sh -e cpu -d 60 -f profile.html <pid>

# Profiler allocations
./profiler.sh -e alloc -d 60 -f alloc.html <pid>

# Profiler lock contention
./profiler.sh -e lock -d 60 -f lock.html <pid>

4.2 Intégration avec IntelliJ

Utiliser le plugin "Async Profiler" intégré dans IntelliJ Ultimate.

5. JMH Benchmarks

5.1 Bonnes pratiques

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
@Fork(3)                    // 3 forks pour éliminer les variations
@Warmup(iterations = 5)     // 5 warmup itérations
@Measurement(iterations = 10) // 10 mesures
public class StringBenchmark {
    
    private String str;
    
    @Setup
    public void setup() {
        str = "Hello World!";
    }
    
    @Benchmark
    public String substring() {
        return str.substring(0, 5);
    }
    
    @Benchmark
    public String split() {
        return str.split(" ")[0];
    }
}

5.2 Dead Code Elimination

@Benchmark
public int baseline() {
    return a + b; // Blackhole implicite si retour utilisé
}

@Benchmark
public void blackhole(Blackhole bh) {
    int result = a + b;
    bh.consume(result); // Empêche la DCE
}

6. Heap Analysis avec Eclipse MAT

6.1 Obtenir un heap dump

# Via jcmd
jcmd <pid> GC.heap_dump /path/to/dump.hprof

# Via jmap
jmap -dump:live,format=b,file=dump.hprof <pid>

# Automatiquement à OOME
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dumps/

6.2 Analyser avec MAT

  • Top Consumers : Quels objets consomment le plus ?
  • Dominator Tree : Quels objets retiennent la mémoire ?
  • GC Roots : Qu'est-ce qui empêche le GC ?
  • Leak Suspects : Rapports automatiques de fuites

6.3 OQL (Object Query Language)

-- Trouver tous les char[] de plus de 1 MB
SELECT * FROM char[] WHERE @usedHeapSize > 1000000

-- Trouver les 10 plus gros objets
SELECT * FROM java.lang.Object ORDER BY @usedHeapSize DESC LIMIT 10

7. Thread Dumps

7.1 Prise de thread dump

# Via jstack
jstack <pid> > threaddump.txt

# Via jcmd
jcmd <pid> Thread.print > threaddump.txt

# Automatique via JFR (jdk.JavaMonitorWait)

7.2 Analyser les threads

// États courants
RUNNABLE      // Thread en exécution
BLOCKED       // Thread en attente d'un monitor
WAITING       // Thread en wait() ou park()
TIMED_WAITING // Thread en wait(timeout)

7.3 Deadlock detection

jstack <pid> | grep -A 30 "Found one Java-level deadlock"

8. Memory Leaks

8.1 Causes fréquentes

  • Static collections : HashMap statique qui ne se vide jamais
  • Listener non deregistré : Observer pattern mal géré
  • ThreadLocals : ThreadLocal non nettoyé dans les pools
  • ClassLoader leak : Redeploiement sans nettoyage
  • Inner classes : Référence implicite vers l'outer class
// Exemple de fuite : static collection
public class OrderCache {
    private static final Map<OrderId, Order> cache = new HashMap<>();
    
    public void addOrder(Order order) {
        cache.put(order.getId(), order); // Jamais nettoyé !
    }
}

8.2 Détection avancée

# Analyse avec JFR
-XX:StartFlightRecording=settings=leak-profile.jfc

# Analyse avec Async Profiler des allocations
./profiler.sh -e alloc -d 120 -f alloc.html <pid>

9. GraalVM

9.1 Native Image

# Installation
gu install native-image

# Compilation native
native-image -jar app.jar --no-fallback \
    --enable-url-protocols=http,https \
    -H:+ReportExceptionStackTraces

9.2 Avantages du native

  • Démarrage instantané (ms vs secondes)
  • Empreinte mémoire réduite (10-50 MB)
  • Pas de warmup JIT nécessaire

9.3 Limitations

  • Reflection doit être configurée
  • Pas de proxy dynamique
  • Pas de chargement de classes dynamique
  • Certaines fonctionnalités Spring nécessitent des hints
// Exemple de reflection hint pour GraalVM
@TypeHint(types = Order.class, fields = true)
@ReflectionHint(name = "com.orderhub.domain.model.*")
public class GraalVMConfig {}

9.4 AOT Compilation

# Compilation AOT avancée
native-image --features=... \
    --initialize-at-build-time=... \
    --trace-class-initialization=... \
    -H:IncludeResources=... \
    -H:ReflectionConfigurationFiles=...

Points clés

  • Comprendre la JVM est essentiel pour optimiser
  • Le choix du GC dépend des besoins (latence vs throughput)
  • JFR est un outil de profiling intégré sans overhead
  • Async Profiler donne des flame graphs précis
  • JMH est indispensable pour les microbenchmarks fiables
  • MAT analyse les heap dumps et trouve les fuites
  • Les thread dumps aident à diagnostiquer les blocages
  • GraalVM native-image permet des démarrages instantanés