Comprendre les fuites de mémoire en Java

Comprendre les fuites de mémoire en Java

1. introduction

L'un des principaux avantages de Java est la gestion automatisée de la mémoire à l'aide du Garbage Collector intégré (ouGC en abrégé). Le GC prend implicitement soin d'allouer et de libérer de la mémoire et est donc capable de gérer la plupart des problèmes de fuite de mémoire.

Bien que le GC gère efficacement une bonne partie de la mémoire, il ne garantit pas une solution infaillible aux fuites de mémoire. Le GC est assez intelligent, mais pas impeccable. Les fuites de mémoire peuvent toujours se glisser même dans les applications d'un développeur consciencieux.

Il peut encore y avoir des situations où l’application génère un nombre important d’objets superflus, épuisant ainsi des ressources mémoire cruciales, entraînant parfois l’échec de l’ensemble de l’application.

Les fuites de mémoire sont un véritable problème en Java. Dans ce tutoriel, nous verronswhat the potential causes of memory leaks are, how to recognize them at runtime, and how to deal with them in our application.

2. Qu'est-ce qu'une fuite de mémoire

Une fuite de mémoire est une situationwhen there are objects present in the heap that are no longer used, but the garbage collector is unable to remove them from memory et, par conséquent, ils sont inutilement maintenus.

Une fuite de mémoire est mauvaise car elleblocks memory resources and degrades system performance over time. Et si elle n'est pas traitée, l'application finira par épuiser ses ressources, se terminant finalement par unjava.lang.OutOfMemoryError fatal.

Deux types d'objets différents résident dans la mémoire de tas - référencés et non référencés. Les objets référencés sont ceux qui ont encore des références actives dans l'application, tandis que les objets non référencés n'ont pas de références actives.

The garbage collector removes unreferenced objects periodically, but it never collects the objects that are still being referenced. C'est là que des fuites de mémoire peuvent se produire:

 

image

Symptômes d'une fuite de mémoire

  • Sévère dégradation des performances lorsque l'application est exécutée en continu pendant une longue période

  • Erreur de tas deOutOfMemoryError dans l'application

  • Application spontanée et étrange se bloque

  • L'application est parfois à court d'objets de connexion

Examinons de plus près certains de ces scénarios et comment y faire face.

3. Types de fuites de mémoire en Java

Dans toute application, les fuites de mémoire peuvent survenir pour de nombreuses raisons. Dans cette section, nous aborderons les plus courants.

3.1. Fuite de mémoire à travers les champsstatic

Le premier scénario qui peut provoquer une fuite de mémoire potentielle est une utilisation intensive des variablesstatic.

En Java,static fields have a life that usually matches the entire lifetime of the running application (sauf siClassLoader devient éligible pour le garbage collection).

Créons un programme Java simple qui remplit unstaticList:

public class StaticTest {
    public static List list = new ArrayList<>();

    public void populateList() {
        for (int i = 0; i < 10000000; i++) {
            list.add(Math.random());
        }
        Log.info("Debug Point 2");
    }

    public static void main(String[] args) {
        Log.info("Debug Point 1");
        new StaticTest().populateList();
        Log.info("Debug Point 3");
    }
}

Maintenant, si nous analysons la mémoire du tas pendant l’exécution de ce programme, alors nous verrons qu’entre les points de débogage 1 et 2, comme prévu, la mémoire du tas a augmenté.

Mais lorsque nous laissons la méthodepopulateList() au point de débogage 3,the heap memory isn’t yet garbage collected comme nous pouvons le voir dans cette réponse VisualVM:

 

image

Cependant, dans le programme ci-dessus, à la ligne numéro 2, si nous supprimons simplement le mot-cléstatic, cela apportera un changement radical à l'utilisation de la mémoire, cette réponse Visual VM montre:

 

image

La première partie jusqu'au point de débogage est presque la même que ce que nous avons obtenu dans le cas destatic. Mais cette fois, après avoir quitté la méthodepopulateList(),all the memory of the list is garbage collected because we don’t have any reference to it.

Par conséquent, nous devons être très attentifs à notre utilisation des variablesstatic. Si des collections ou des objets volumineux sont déclarés en tant questatic, ils restent en mémoire pendant toute la durée de vie de l'application, bloquant ainsi la mémoire vitale qui pourrait autrement être utilisée ailleurs.

Comment l'empêcher?

  • Minimiser l'utilisation des variablesstatic

  • Lorsque vous utilisez des singletons, utilisez une implémentation qui charge l’objet paresseusement plutôt que de le charger avec impatience.

3.2. Grâce à des ressources non fermées

Chaque fois que nous établissons une nouvelle connexion ou ouvrons un flux, la JVM alloue de la mémoire pour ces ressources. Quelques exemples incluent les connexions à la base de données, les flux d’entrée et les objets de session.

Oublier de fermer ces ressources peut bloquer la mémoire et ainsi les garder hors de portée du CPG. Cela peut même se produire en cas d'exception qui empêche l'exécution du programme d'atteindre l'instruction qui gère le code pour fermer ces ressources.

Dans les deux cas,the open connection left from resources consumes memory, et si nous ne les traitons pas, ils peuvent détériorer les performances et même entraîner desOutOfMemoryError.

Comment l'empêcher?

  • Utilisez toujours le blocfinally pour fermer les ressources

  • Le code (même dans le blocfinally) qui ferme les ressources ne doit pas lui-même avoir d'exceptions

  • Lorsque vous utilisez Java 7+, nous pouvons utiliser le bloctry-with-resources

3.3. Implémentations deequals() ethashCode() incorrectes

Lors de la définition de nouvelles classes, une erreur très courante consiste à ne pas écrire de méthodes remplacées appropriées pour les méthodesequals() ethashCode().

HashSet etHashMap utilisent ces méthodes dans de nombreuses opérations, et si elles ne sont pas remplacées correctement, elles peuvent devenir une source de problèmes potentiels de fuite de mémoire.

Prenons un exemple d'une classePerson triviale et utilisons-la comme clé dans unHashMap:

public class Person {
    public String name;

    public Person(String name) {
        this.name = name;
    }
}

Nous allons maintenant insérer des objetsPerson en double dans unMap qui utilise cette clé.

N'oubliez pas qu'unMap ne peut pas contenir de clés en double:

@Test
public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {
    Map map = new HashMap<>();
    for(int i=0; i<100; i++) {
        map.put(new Person("jon"), 1);
    }
    Assert.assertFalse(map.size() == 1);
}

Ici, nous utilisonsPerson comme clé. Étant donné queMap n'autorise pas les clés en double, les nombreux objetsPerson en double que nous avons insérés en tant que clé ne devraient pas augmenter la mémoire.

Maissince we haven’t defined proper equals() method, the duplicate objects pile up and increase the memory, c'est pourquoi nous voyons plus d'un objet dans la mémoire. La mémoire de tas dans VisualVM pour cela ressemble à ceci:

 

image

Cependant,if we had overridden the equals() and hashCode() methods properly, then there would only exist one Person object in this*Map*.

Jetons un coup d'œil aux implémentations appropriées deequals() ethashCode() pour notre classePerson:

public class Person {
    public String name;

    public Person(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof Person)) {
            return false;
        }
        Person person = (Person) o;
        return person.name.equals(name);
    }

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + name.hashCode();
        return result;
    }
}

Et dans ce cas, les assertions suivantes seraient vraies:

@Test
public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {
    Map map = new HashMap<>();
    for(int i=0; i<2; i++) {
        map.put(new Person("jon"), 1);
    }
    Assert.assertTrue(map.size() == 1);
}

Après avoir correctement remplacéequals() ethashCode(), la mémoire du tas pour le même programme ressemble à ceci:

 

image

Un autre exemple est l'utilisation d'un outil ORM comme Hibernate, qui utilise les méthodesequals() ethashCode() pour analyser les objets et les enregistrer dans le cache.

The chances of memory leak are quite high if these methods are not overridden car Hibernate ne serait alors pas en mesure de comparer les objets et remplirait son cache avec des objets en double.

Comment l'empêcher?

  • En règle générale, lors de la définition de nouvelles entités, remplacez toujours les méthodesequals() ethashCode()

  • Il ne suffit pas de remplacer ces méthodes, mais ces méthodes doivent également être remplacées de manière optimale

Pour plus d'informations, visitez nos tutorielsGenerate equals() and hashCode() with Eclipse etGuide to hashCode() in Java.

3.4. Classes internes faisant référence aux classes externes

Cela se produit dans le cas de classes internes non statiques (classes anonymes). Pour l'initialisation, ces classes internes nécessitent toujours une instance de la classe englobante.

Chaque classe interne non statique a, par défaut, une référence implicite à la classe qui la contient. Si nous utilisons l'objet de cette classe interne dans notre application, alorseven after our containing class' object goes out of scope, it will not be garbage collected.

Prenons une classe qui contient la référence à de nombreux objets volumineux et possède une classe interne non statique. Maintenant, lorsque nous créons un objet de la classe interne uniquement, le modèle de mémoire se présente comme suit:

 

image

Cependant, si nous déclarons simplement la classe interne en tant que statique, le même modèle de mémoire ressemble à ceci:

image

Cela est dû au fait que l'objet de classe interne contient implicitement une référence à l'objet de classe externe, ce qui en fait un candidat non valide pour le garbage collection. La même chose se produit dans le cas des classes anonymes.

Comment l'empêcher?

  • Si la classe interne n'a pas besoin d'accéder aux membres de la classe contenant, envisagez de la transformer en une classestatic

3.5. Par les méthodesfinalize()

L'utilisation de finaliseurs est une autre source potentielle de problèmes de fuite de mémoire. Chaque fois qu'une méthode de classe 'finalize() est remplacée, alorsobjects of that class aren’t instantly garbage collected. Au lieu de cela, le GC les met en file d'attente pour la finalisation, ce qui se produit à un moment ultérieur.

De plus, si le code écrit dans la méthodefinalize() n'est pas optimal et si la file d'attente du finaliseur ne peut pas suivre le garbage collector Java, alors tôt ou tard, notre application est destinée à rencontrer unOutOfMemoryError.

Pour illustrer cela, considérons que nous avons une classe pour laquelle nous avons remplacé la méthodefinalize() et que la méthode prend un peu de temps à s'exécuter. Lorsqu'un grand nombre d'objets de cette classe est récupéré, dans VisualVM, cela ressemble à:

 

image

Cependant, si nous supprimons simplement la méthodefinalize() remplacée, le même programme donne la réponse suivante:

image

Comment l'empêcher?

  • Nous devrions toujours éviter les finaliseurs

Pour plus de détails surfinalize(), lisez la section 3 (Avoiding Finalizers) in ourGuide to the finalize Method in Java.

3.6. InternésStrings

Le pool JavaString avait subi un changement majeur dans Java 7 lors de son transfert de PermGen vers HeapSpace. Mais pour les applications fonctionnant sur les versions 6 et inférieures, nous devrions être plus attentifs lorsque nous travaillons avec de grandsStrings.

If we read a huge massive String object, and call intern() on that object, then it goes to the string pool, which is located in PermGen (permanent memory) and will stay there as long as our application runs. Cela bloque la mémoire et crée une fuite de mémoire majeure dans notre application.

Le PermGen pour ce cas dans JVM 1.6 ressemble à ceci dans VisualVM:

 

image

Contrairement à cela, dans une méthode, si on se contente de lire une chaîne dans un fichier et de ne pas l'interner, le PermGen ressemble à ceci:

image

 

Comment l'empêcher?

  • Le moyen le plus simple de résoudre ce problème consiste à effectuer une mise à niveau vers la dernière version de Java, car le pool de chaînes est déplacé vers HeapSpace à partir de Java version 7.

  • Si vous travaillez sur de grandsStrings, augmentez la taille de l'espace PermGen pour éviter tout potentielOutOfMemoryErrors:

    -XX:MaxPermSize=512m

3.7. Utilisation deThreadLocals

ThreadLocal (discuté en détail dans le tutoriel deIntroduction to ThreadLocal in Java) est une construction qui nous donne la possibilité d'isoler l'état d'un thread particulier et nous permet ainsi d'atteindre la sécurité des threads.

Lors de l'utilisation de cette construction,each thread will hold an implicit reference to its copy of a ThreadLocal variable and will maintain its own copy, instead of sharing the resource across multiple threads, as long as the thread is alive.

Malgré ses avantages, l'utilisation des variablesThreadLocal est controversée, car elles sont tristement célèbres pour introduire des fuites de mémoire si elles ne sont pas utilisées correctement. Joshua Blochonce commented on thread local usage:

“L'utilisation négligée de pools de threads en combinaison avec l'utilisation négligente de localisations de threads peut entraîner une rétention d'objet inattendue, comme cela a été noté à de nombreux endroits. Mais faire porter le blâme sur les locaux du fil est injustifié. "

Fuite de mémoire avecThreadLocals

ThreadLocals sont censés être récupérés une fois que le thread en attente n'est plus actif. Mais le problème survient lorsqueThreadLocals est utilisé avec des serveurs d'applications modernes.

Les serveurs d'applications modernes utilisent un pool de threads pour traiter les requêtes au lieu d'en créer de nouvelles (par exemplethe Executor dans le cas d'Apache Tomcat). De plus, ils utilisent également un chargeur de classe séparé.

Étant donné queThread Pools dans les serveurs d’applications fonctionne sur le concept de réutilisation des threads, ils ne sont jamais vidés de leur mémoire, mais ils sont réutilisés pour répondre à une autre requête.

Maintenant, si une classe crée une variableThreadLocal mais ne la supprime pas explicitement, alors une copie de cet objet restera avec le workerThread même après l'arrêt de l'application Web, empêchant ainsi l'objet d'être ordures collectées.

Comment l'empêcher?

  • Il est recommandé de nettoyer lesThreadLocals lorsqu'ils ne sont plus utilisés -ThreadLocals fournit la méthoderemove(), qui supprime la valeur du thread actuel pour cette variable

  • Do not use ThreadLocal.set(null) to clear the value - il n'efface pas réellement la valeur mais recherchera à la place lesMap associés au thread actuel et définira la paire clé-valeur comme thread actuel etnull respectivement

  • Il est encore mieux de considérerThreadLocal  comme une ressource qui doit être fermée dans un blocfinally juste pour s’assurer qu’elle est toujours fermée, même en cas d’exception:

    try {
        threadLocal.set(System.nanoTime());
        //... further processing
    }
    finally {
        threadLocal.remove();
    }

4. Autres stratégies pour traiter les fuites de mémoire

Bien qu'il n'existe pas de solution unique pour les fuites de mémoire, il existe plusieurs moyens de minimiser ces fuites.

4.1. Activer le profilage

Les profileurs Java sont des outils qui surveillent et diagnostiquent les fuites de mémoire via l'application. Ils analysent ce qui se passe en interne dans notre application - par exemple, comment la mémoire est allouée.

À l'aide de profileurs, nous pouvons comparer différentes approches et trouver des domaines dans lesquels nous pouvons utiliser nos ressources de manière optimale.

Nous avons utiliséJava VisualVM tout au long de la section 3 de ce didacticiel. Veuillez consulter nosGuide to Java Profilers pour en savoir plus sur les différents types de profileurs, tels que Mission Control, JProfiler, YourKit, Java VisualVM et Netbeans Profiler.

4.2. Ramassage des ordures détaillé

En activant le garbage collection détaillé, nous suivons une trace détaillée du GC. Pour l'activer, nous devons ajouter les éléments suivants à notre configuration JVM:

-verbose:gc

En ajoutant ce paramètre, nous pouvons voir les détails de ce qui se passe dans GC:

image

 

4.3. Utiliser des objets de référence pour éviter les fuites de mémoire

Nous pouvons également recourir à des objets de référence en Java qui sont intégrés au packagejava.lang.ref pour gérer les fuites de mémoire. En utilisant le packagejava.lang.ref, au lieu de référencer directement des objets, nous utilisons des références spéciales aux objets qui leur permettent d'être facilement récupérés.

Les files de référence sont conçues pour nous informer des actions effectuées par le récupérateur de place. Pour plus d'informations, lisez l'exemple de tutoriel deSoft References in Java, en particulier la section 4.

4.4. Avertissements de fuite de mémoire Eclipse

Pour les projets sur JDK 1.5 et versions ultérieures, Eclipse affiche des avertissements et des erreurs chaque fois qu'il rencontre des cas évidents de fuites de mémoire. Ainsi, lors du développement dans Eclipse, nous pouvons régulièrement consulter l'onglet «Problèmes» et être plus vigilant à propos des avertissements de fuite de mémoire (le cas échéant):

image

 

4.5. Benchmarking

Nous pouvons mesurer et analyser les performances du code Java en exécutant des benchmarks. De cette façon, nous pouvons comparer les performances d’approches alternatives pour effectuer la même tâche. Cela peut nous aider à choisir une meilleure approche et à conserver notre mémoire.

Pour plus d'informations sur l'analyse comparative, veuillez consulter notre tutorielMicrobenchmarking with Java.

4.6. Revues de code

Enfin, nous avons toujours la manière classique et classique de faire un simple tour de code.

Dans certains cas, même cette méthode triviale peut aider à éliminer certains problèmes courants de fuite de mémoire.

5. Conclusion

En termes simples, nous pouvons considérer la fuite de mémoire comme une maladie qui dégrade les performances de notre application en bloquant les ressources mémoire vitales. Et comme toutes les autres maladies, si elles ne sont pas guéries, elles peuvent entraîner des accidents mortels au fil du temps.

Les fuites de mémoire sont difficiles à résoudre et leur recherche nécessite une maîtrise et une maîtrise complexes du langage Java. While dealing with memory leaks, there is no one-size-fits-all solution, as leaks can occur through a wide range of diverse events.

Toutefois, si nous recourons aux meilleures pratiques et effectuons régulièrement des contrôles détaillés du profil et du code, nous pouvons réduire le risque de fuite de mémoire dans notre application.

Comme toujours, les extraits de code utilisés pour générer les réponses VisualVM décrites dans ce didacticiel sont disponibleson GitHub.