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