1. Introduction au profilage
Le profilage — c’est comme un examen médical pour votre programme: on ne regarde pas seulement la « température » (monitoring), on cherche où l’application « a mal », ce qui fonctionne lentement, où trop de mémoire ou de ressources sont consommées.
Le profilage est le processus de collecte et d’analyse d’informations sur l’exécution d’un programme afin d’identifier les goulots d’étranglement (bottlenecks) et les parties de code inefficaces. Contrairement au monitoring, qui suit généralement des indicateurs globaux (charge CPU, mémoire, nombre de threads), le profilage permet de regarder à l’intérieur: savoir quelles méthodes sont appelées le plus souvent, combien de temps elles prennent, combien d’objets sont créés et où se produit exactement une fuite de mémoire.
Quand le profilage est-il réellement nécessaire ?
- L’application « rame », sans raison apparente.
- La consommation mémoire a soudainement augmenté.
- Après une mise à jour du code, quelque chose est devenu plus lent.
- Il faut comprendre pourquoi le serveur manque de ressources.
Au passage, presque tous les développeurs ont optimisé au moins une fois la mauvaise portion de code. Pourquoi ? Parce qu’il est quasiment impossible d’identifier le goulot d’étranglement « à l’œil » — c’est pour cela qu’un profileur est nécessaire.
Métriques principales du profilage
- Temps d’exécution des méthodes (profilage CPU): Quelles méthodes prennent le plus de temps ? Où le programme « consomme » du processeur ?
- Utilisation de la mémoire (profilage mémoire): Quels objets sont créés le plus souvent ? Où restent-ils en mémoire plus longtemps que nécessaire ?
- Nombre d’objets: Ne créons-nous pas trop d’objets du même type ?
- Threads: N’y a-t-il pas trop de threads ? Y a-t-il des blocages (deadlock, contention) ?
- Appels de méthodes: Quelle est la profondeur de la pile ? Une récursion sans fin se produit-elle ?
2. Outils de profilage
Dans l’univers Java, il existe plusieurs outils classiques (et gratuits !) permettant de profiler. Voyons les principaux.
VisualVM
VisualVM est un outil gratuit, inclus dans le JDK (à partir de JDK 6). Il permet de :
- Se connecter à des JVM locales et distantes.
- Observer la mémoire, les threads, le CPU, le garbage collector.
- Effectuer un heap dump et l’analyser.
- Profiler l’application par CPU et mémoire.
Comment lancer VisualVM ?
Il se trouve généralement dans le dossier du JDK : <path_to_JDK>/bin/jvisualvm
Lancez-le, sélectionnez le processus de l’application Java — et vous pouvez observer sa vie, comme des poissons dans un aquarium (sauf qu’ici, les « poissons » sont des objets et des threads).
JProfiler, YourKit
Ce sont des outils commerciaux, mais très puissants. Ils permettent de :
- Profiler la mémoire, le CPU, les threads.
- Analyser des « instantanés » mémoire (heap dump).
- Chercher des fuites, de longs verrouillages, des méthodes lentes.
- S’intégrer aux IDE et aux chaînes CI/CD.
Pour débuter, VisualVM suffit, mais si vous passez à de grands projets — intéressez-vous à ces outils.
Java Flight Recorder (JFR)
JFR est un outil intégré au JDK pour collecter des événements sur le fonctionnement de la JVM. Il est très léger, impacte très peu les performances, et permet de collecter des informations sur :
- Le temps d’exécution des méthodes.
- Le garbage collector.
- Les threads, les blocages, les erreurs.
JFR convient parfaitement à la production, quand on ne peut pas ralentir l’application.
3. Pratique : profiler une application simple
Créons un mini-calculateur capable d’effectuer de longs calculs et de conserver l’historique des opérations (pour avoir des boucles, des collections et du travail avec la mémoire).
Exemple de code : « Calculateur lent »
import java.util.ArrayList;
import java.util.List;
public class SlowCalculator {
private final List<String> history = new ArrayList<>();
public int add(int a, int b) {
simulateHeavyOperation();
int result = a + b;
history.add(a + " + " + b + " = " + result);
return result;
}
public int multiply(int a, int b) {
simulateHeavyOperation();
int result = a * b;
history.add(a + " * " + b + " = " + result);
return result;
}
public List<String> getHistory() {
return history;
}
// Simulation d'une opération "lourde"
private void simulateHeavyOperation() {
for (int i = 0; i < 5_000_000; i++) {
Math.sqrt(i);
}
}
}
Et maintenant — la classe principale :
public class Main {
public static void main(String[] args) {
SlowCalculator calc = new SlowCalculator();
for (int i = 0; i < 10; i++) {
calc.add(i, i * 2);
calc.multiply(i, i + 5);
}
System.out.println("Historique des opérations:");
for (String entry : calc.getHistory()) {
System.out.println(entry);
}
}
}
Comment profiler cette application ?
- Compilez et exécutez l’application.
- Ouvrez VisualVM (jvisualvm).
- Trouvez votre processus (généralement par le nom de la classe Main).
- Allez dans l’onglet CPU Profiler et cliquez sur Start.
- Laissez le programme tourner (ou relancez l’opération lente).
- Regardez quelles méthodes prennent le plus de temps.
Question : À votre avis, quelle méthode sera la plus « lourde » ?
Réponse : Bien sûr, simulateHeavyOperation() — elle exécute une énorme boucle de 5_000_000 itérations et appelle Math.sqrt.
4. Problèmes de performance typiques
Algorithmes lents
La raison la plus courante : un mauvais choix d’algorithme ou de structure de données. Par exemple, une recherche dans une liste au lieu d’utiliser une HashMap, ou un tri à bulles au lieu d’un tri rapide.
Exemple :
// Recherche lente
for (String s : list) {
if (s.equals("target")) {
// trouvé
}
}
Il vaut mieux utiliser un Set ou une Map pour une recherche rapide.
Fuites de mémoire
Une fuite de mémoire est une situation où des objets restent « vivants » (ils sont référencés) alors qu’ils ne sont plus nécessaires. Cela entraîne une augmentation de la consommation mémoire et, finalement, un OutOfMemoryError.
public class MemoryLeakDemo {
private static List<byte[]> leakyList = new ArrayList<>();
public static void main(String[] args) {
while (true) {
leakyList.add(new byte[1_000_000]); // 1 Mo
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
}
}
}
Comment trouver les fuites ?
Faites un heap dump dans VisualVM et voyez quels objets occupent le plus de mémoire et pourquoi ils sont référencés.
Création excessive d’objets
Si vous créez beaucoup d’objets du même type dans une boucle, cela ne surcharge pas seulement le garbage collector, mais peut aussi ralentir l’application.
for (int i = 0; i < 1_000_000; i++) {
String s = new String("hello"); // mauvais!
}
Mieux vaut utiliser des constantes ou le pool de chaînes (String pool).
Verrouillages de threads
Si plusieurs threads se disputent la même ressource (par exemple, une méthode synchronisée), cela peut entraîner des blocages et une chute des performances.
public synchronized void doWork() {
// ...
}
Comment les détecter ?
Dans l’onglet Threads de VisualVM, vous pouvez voir quels threads « pendent » et pourquoi.
5. Approches d’optimisation
Mesurer d’abord, optimiser ensuite
Règle d’or de l’optimisation : N’optimisez pas ce qui ne ralentit pas.
Commencez par profiler, trouvez les « hot spots », puis modifiez le code. Parfois, la portion de code la plus « évidente » ne représente que 1 % du temps, alors que le véritable « monstre » se cache dans une bibliothèque ou un endroit inattendu.
Utiliser un profileur pour trouver les hot spots
Un hot spot est une méthode ou une portion de code qui occupe la plus grande part du temps d’exécution de l’application.
Dans VisualVM, c’est visible dans l’onglet CPU Profiler :
- Triez les méthodes par temps d’exécution.
- Regardez le stack trace : qui appelle qui.
- Souvenez-vous que parfois, « le coupable » n’est pas votre code, mais une bibliothèque, voire le JDK.
Exemples d’optimisation
Exemple 1 : Remplacer l’algorithme
Si vous constatez que la majeure partie du temps est passée à rechercher dans une liste, remplacez List par HashSet.
Set<String> set = new HashSet<>(list);
if (set.contains("target")) {
// rapide!
}
Exemple 2 : Réduire le nombre d’allocations
Au lieu de créer de nouveaux objets dans une boucle, réutilisez-les ou utilisez StringBuilder.
// Mauvais:
for (int i = 0; i < 10000; i++) {
String s = "Résultat: " + i;
}
// Mieux:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.setLength(0);
sb.append("Résultat: ").append(i);
String s = sb.toString();
}
Exemple 3 : Mise en cache
Si vous voyez qu’une méthode lourde est appelée de nombreuses fois avec les mêmes paramètres, utilisez un cache.
Map<Integer, Double> sqrtCache = new HashMap<>();
public double cachedSqrt(int x) {
return sqrtCache.computeIfAbsent(x, Math::sqrt);
}
6. Démonstration : accélérer notre calculateur
Problème : simulateHeavyOperation() consomme beaucoup de temps
Étape 1. Profiler
Dans VisualVM, on voit que presque tout le temps est passé dans Math.sqrt(i) à l’intérieur d’une boucle de 5_000_000 itérations.
Étape 2. Optimiser
Si ce n’est qu’une simulation de charge — supprimez-la ou réduisez le nombre d’itérations.
Si c’est une logique métier réelle — demandez-vous s’il est possible de :
- Mettre en cache le résultat.
- Utiliser un algorithme plus rapide.
- Déplacer les calculs dans un thread séparé (si ce n’est pas critique pour l’utilisateur).
Exemple d’optimisation :
private void simulateHeavyOperation() {
// De 5_000_000 à 100_000
for (int i = 0; i < 100_000; i++) {
Math.sqrt(i);
}
}
Étape 3. Vérifier le résultat
Relancez le profilage — le programme fonctionne plus vite, la charge CPU a diminué.
7. Visualisation : processus d’optimisation
flowchart TD
A[Lancement de l'application]
B["Profilage (VisualVM)"]
C[Identification des goulots d'étranglement]
D[Optimisation du code]
E[Nouveau profilage]
F[Amélioration des performances]
A --> B --> C --> D --> E --> F
E --> C
8. Erreurs courantes lors du profilage et de l’optimisation
Erreur n° 1: optimisation « au jugé ». Très souvent, les développeurs commencent à modifier le code sans mesurer où se trouve réellement le problème. Résultat : beaucoup de travail pour un gain minime.
Erreur n° 2: profilage dans des conditions « irréelles ». Il faut profiler avec des données et une charge proches de la production. Un profilage « à vide » peut ne pas révéler les vrais problèmes.
Erreur n° 3: ignorer les fuites de mémoire. Sans regarder le heap dump et sans analyser les références, on peut ne pas remarquer longtemps que le programme « enfle » et tombera bientôt.
Erreur n° 4: poursuite d’optimisations microscopiques. Il ne faut pas passer des jours à accélérer un code qui ne représente que 0.1 % du temps d’exécution de l’application. D’abord — les principaux goulots d’étranglement.
Erreur n° 5: prise en compte insuffisante des threads et de la synchronisation. Dans les applications multithread, les problèmes de performances sont souvent liés non pas aux algorithmes, mais aux verrouillages et aux attentes (synchronized, contention).
Erreur n° 6: oublier de profiler après les changements. Après une optimisation, vérifiez impérativement le résultat : parfois, une « optimisation » peut même ralentir l’exécution !
GO TO FULL VERSION