Fuites mémoire Java : causes courantes et détection

Backend2026-09-27TryQuickToolBox

Votre application Java démarre rapidement mais après quelques heures ou jours, elle ralentit jusqu'à l'arrêt, lance une OutOfMemoryError ou est tuée par le conteneur. Vous la redémarrez, et le cycle se répète. C'est la signature classique d'une fuite mémoire Java : des objets qui ne sont plus nécessaires mais restent accessibles, empêchant le garbage collector de les récupérer. Contrairement au C/C++, le GC de Java gère automatiquement la plupart de la mémoire, mais des fuites surviennent quand des références sont conservées involontairement. Cet article explique les causes les plus courantes et vous donne un workflow pratique pour les détecter et les corriger.

Qu'est-ce qu'une fuite mémoire Java ?

Une fuite mémoire se produit lorsque des objets ne sont plus utilisés par l'application mais sont toujours référencés, empêchant le garbage collection. Avec le temps, l'utilisation du heap augmente, le GC s'exécute plus fréquemment, et finalement la JVM lance OutOfMemoryError: Java heap space. Les fuites peuvent être petites et lentes (quelques Ko par requête) ou grandes et rapides (mise en cache de jeux de résultats entiers).

Causes courantes des fuites mémoire Java

1. Collections statiques qui grandissent indéfiniment

Les champs statiques vivent pendant toute la durée de vie de la JVM. Une Map statique utilisée comme cache sans éviction est une fuite classique. Par exemple :

public class UserCache {
    private static final Map<Long, User> CACHE = new HashMap<>();

    public static void put(Long id, User user) {
        CACHE.put(id, user); // jamais supprimé
    }
}

Chaque utilisateur ajouté reste pour toujours. Correctif : utilisez un cache borné comme Caffeine ou Guava avec des limites de taille et une expiration.

2. Ressources non fermées

Les flux, connexions et lecteurs doivent être fermés. Si vous oubliez, les ressources natives et leurs wrappers Java fuient. Utilisez toujours try-with-resources :

try (InputStream in = new FileInputStream(file)) {
    // utiliser in
} // fermé automatiquement

Cela s'applique aussi aux connexions de base de données, clients HTTP et pools de threads.

3. Listeners et callbacks non supprimés

Quand vous enregistrez un listener (par ex. addListener) mais ne le supprimez jamais, l'émetteur conserve une référence vers le listener, qui peut lui-même référencer tout votre graphe d'objets. C'est courant dans les interfaces graphiques, les bus d'événements et les contextes à longue durée de vie.

4. Variables ThreadLocal

Les valeurs ThreadLocal sont stockées dans le ThreadLocalMap du thread. Dans les pools de threads, les threads sont réutilisés, donc si vous n'appelez pas remove(), la valeur reste attachée au thread et fuit. Nettoyez toujours :

try {
    threadLocal.set(context);
    // travail
} finally {
    threadLocal.remove();
}

5. Classes internes conservant des références externes

Les classes internes non statiques (y compris les classes anonymes) détiennent une référence implicite vers l'instance externe. Si une instance de classe interne survit à l'objet externe (par ex. stockée dans une liste statique), l'objet externe ne peut pas être collecté. Rendez les classes internes statiques quand elles n'ont pas besoin de l'instance externe.

6. equals() et hashCode() incorrects

Si vous utilisez des objets comme clés dans une HashMap mais redéfinissez equals() sans hashCode() (ou inversement), les recherches échouent et les entrées s'accumulent. Redéfinissez toujours les deux de manière cohérente.

7. String interning et sous-chaînes

Avant Java 7, String.substring() partageait le tableau de caractères original, donc une petite sous-chaîne pouvait retenir un grand tableau. Le Java moderne copie, mais interner de nombreuses chaînes uniques (par ex. String.intern()) peut encore remplir le pool de chaînes.

Comment détecter les fuites mémoire Java

La détection suit un processus systématique. Voici une approche étape par étape :

  1. Surveillez l'utilisation du heap dans le temps. Utilisez JVisualVM, JConsole ou votre APM. Une application saine montre un motif en dents de scie : le heap grandit, le GC récupère, le heap diminue. Une fuite montre une ligne de base qui monte après chaque GC.
  2. Activez les logs GC. Ajoutez -Xlog:gc*:file=gc.log:time,uptime,level,tags (Java 9+) ou -XX:+PrintGCDetails (Java 8). Analysez les logs avec des outils comme GCeasy pour voir si l'old gen continue de croître.
  3. Capturez un heap dump. Quand le heap est élevé, exécutez jmap -dump:live,format=b,file=heap.hprof <pid> ou utilisez jcmd <pid> GC.heap_dump heap.hprof. Vous pouvez aussi déclencher un dump sur OutOfMemoryError avec -XX:+HeapDumpOnOutOfMemoryError.
  4. Analysez le heap dump. Ouvrez-le dans Eclipse MAT ou VisualVM. Cherchez le dominator tree pour trouver les objets qui retiennent le plus de mémoire. Vérifiez les collections suspectes avec de grandes tailles retenues.
  5. Prenez un second dump et comparez. Si les mêmes objets augmentent entre les dumps, vous avez trouvé la fuite.
  6. Utilisez un profiler pour l'analyse en direct. Des outils comme YourKit, JProfiler ou async-profiler peuvent suivre les sites d'allocation et les chaînes de références sans arrêter l'application.

Voici une comparaison rapide des outils courants :

OutilTypeIdéal pour
Eclipse MATAnalyseur de heap dumpTrouver les dominators et suspects de fuite
VisualVMMonitoring + heap dumpVérifications rapides en direct et analyse de base
JProfiler / YourKitProfilerSuivi des allocations et chaînes de références
async-profilerProfiler à faible surcoûtProfilage en production avec flame graphs

Corriger et prévenir les fuites

Une fois la fuite identifiée, corrigez-la en supprimant la référence involontaire. Correctifs courants :

Mieux vaut prévenir que guérir. Ajoutez la surveillance mémoire à votre pipeline CI/CD, exécutez des tests de charge avec vérification du heap, et définissez -Xmx de manière appropriée. Utilisez des outils d'analyse statique comme SpotBugs pour détecter les motifs courants.

FAQ

Quelle est la différence entre une fuite mémoire et un pic mémoire ?

Un pic mémoire est une augmentation temporaire de l'utilisation du heap que le GC récupère. Une fuite est une augmentation régulière dans le temps car les objets restent accessibles et ne peuvent pas être collectés.

Une fuite mémoire Java peut-elle causer OutOfMemoryError même si le heap est grand ?

Oui. Si le taux de fuite dépasse la capacité du GC à récupérer, le heap finira par se remplir quelle que soit sa taille. Augmenter le heap ne fait que retarder la défaillance.

Comment trouver une fuite mémoire en production sans interruption ?

Utilisez des outils à faible surcoût comme async-profiler ou JFR (Java Flight Recorder) pour enregistrer les allocations et les statistiques du heap. Vous pouvez aussi déclencher un heap dump avec jcmd pendant que l'application tourne, bien que cela puisse provoquer une brève pause.

Besoin d'analyser des logs GC ou d'autres logs texte ? Essayez notre Nginx Log Analyzer pour parser et visualiser rapidement les motifs de logs.