web-dev-qa-db-fra.com

ThreadLocal et fuite de mémoire

Il est mentionné dans plusieurs articles: une mauvaise utilisation de ThreadLocal provoque une fuite de mémoire. J'ai du mal à comprendre comment une fuite de mémoire se produirait en utilisant ThreadLocal.

Le seul scénario que j'ai compris comme ci-dessous:

Un serveur Web gère un pool de threads (par exemple pour les servlets). Ces threads peuvent créer une fuite de mémoire si les variables dans ThreadLocal ne sont pas supprimées car les threads ne meurent pas.

Ce scénario ne mentionne pas de fuite de mémoire "Perm Space". Est-ce le seul cas d'utilisation (majeur) de fuite de mémoire?

47
Sandeep Jindal

Les épuisements PermGen en combinaison avec ThreadLocal sont souvent causés par les fuites du chargeur de classe .

Un exemple:
Imaginez un serveur d'applications qui possède un pool de threads de travail .
Ils seront maintenus en vie jusqu'à la fin du serveur d'applications.
Une application Web déployée utilise un statique ThreadLocal dans l'une de ses classes afin de stocker des données locales de thread , une instance d'une autre classe (appelons-la SomeClass) de l'application Web. Cela se fait dans le thread de travail (par exemple, cette action provient d'une requête HTTP ).

Important:
Par définition , une référence à une valeur ThreadLocal est conservée jusqu'à ce que le thread "propriétaire" meure ou si le ThreadLocal lui-même n'est plus accessible.

Si l'application Web ne parvient pas à effacer la référence au ThreadLocal sur arrêt , de mauvaises choses se produiront:
Parce que le thread de travail ne mourra généralement jamais et que la référence à ThreadLocal est statique, la valeur ThreadLocal fait toujours référence l'instance de SomeClass, la classe d'une application web - même si l'application web a été arrêtée!

Par conséquent, le chargeur de classe de l'application Web ne peut pas être récupéré , ce qui signifie que toutes les classes (et toutes les données statiques) de l'application Web restent chargées (cela affecte le pool de mémoire PermGen ainsi que le tas).
Chaque itération de redéploiement de l'application Web augmentera l'utilisation du permgen (et du tas).

=> Ceci est la fuite permgen

Un exemple populaire de ce type de fuite est ce bogue dans log4j (corrigé entre-temps).

68
MRalwasser

La réponse acceptée à cette question et les journaux "sévères" de Tomcat à ce sujet sont trompeurs. La citation clé est la suivante:

Par définition, une référence à une valeur ThreadLocal est conservée jusqu'à ce que le thread "propriétaire" meurt ou si le ThreadLocal lui-même n'est plus accessible. [Mon accent].

Dans ce cas, les seules références à ThreadLocal se trouvent dans le champ final statique d'une classe qui est maintenant devenue une cible pour GC et la référence à partir des threads de travail. Cependant, les références des threads de travail au ThreadLocal sont WeakReferences !

Cependant, les valeurs d'un ThreadLocal ne sont pas des références faibles. Donc, si vous avez des références dans les valeurs d'un ThreadLocal aux classes d'application, celles-ci conserveront une référence au ClassLoader et empêcheront GC. Cependant, si vos valeurs ThreadLocal ne sont que des entiers ou des chaînes ou un autre type d'objet de base (par exemple, une collection standard de ce qui précède), il ne devrait pas y avoir de problème (ils empêcheront uniquement le GC du chargeur de classe de démarrage/système, qui est ne se produira jamais de toute façon).

Il est toujours recommandé de nettoyer explicitement un ThreadLocal lorsque vous en avez terminé, mais dans le cas de le bogue log4j cité le ciel ne tombait définitivement pas (comme vous pouvez le voir dans le rapport, le est une table de hachage vide).

Voici un code à démontrer. Tout d'abord, nous créons une implémentation de chargeur de classe personnalisée de base sans parent qui imprime sur System.out lors de la finalisation:

import Java.net.*;

public class CustomClassLoader extends URLClassLoader {

    public CustomClassLoader(URL... urls) {
        super(urls, null);
    }

    @Override
    protected void finalize() {
        System.out.println("*** CustomClassLoader finalized!");
    }
}

Nous définissons ensuite une application de pilote qui crée une nouvelle instance de ce chargeur de classe, l'utilise pour charger une classe avec un ThreadLocal puis supprimons la référence au chargeur de classe lui permettant d'être GC'ed. Tout d'abord, dans le cas où la valeur ThreadLocal est une référence à une classe chargée par le chargeur de classe personnalisé:

import Java.net.*;

public class Main {

    public static void main(String...args) throws Exception {
        loadFoo();
        while (true) { 
            System.gc();
            Thread.sleep(1000);
        }
    }

    private static void loadFoo() throws Exception {
        CustomClassLoader cl = new CustomClassLoader(new URL("file:/tmp/"));
        Class<?> clazz = cl.loadClass("Main$Foo");
        clazz.newInstance();
        cl = null;
    }


    public static class Foo {
        private static final ThreadLocal<Foo> tl = new ThreadLocal<Foo>();

        public Foo() {
            tl.set(this);
            System.out.println("ClassLoader: " + this.getClass().getClassLoader());
        }
    }
}

Lorsque nous l'exécutons, nous pouvons voir que CustomClassLoader n'est en effet pas récupéré (car le thread local dans le thread principal a une référence à une instance de Foo qui a été chargée par notre chargeur de classe personnalisé):

 $ Java Main 
 ClassLoader: CustomClassLoader @ 7a6d084b 

Cependant, lorsque nous modifions ThreadLocal pour contenir à la place une référence à un entier simple plutôt qu'à une instance de Foo:

public static class Foo {
    private static final ThreadLocal<Integer> tl = new ThreadLocal<Integer>();

    public Foo() {
        tl.set(42);
        System.out.println("ClassLoader: " + this.getClass().getClassLoader());
    }
}

Ensuite, nous voyons que le chargeur de classe personnalisé est désormais récupéré (car le thread local sur le thread principal n'a qu'une référence à un entier chargé par le chargeur de classe système ):

 $ Java Main 
 ClassLoader: CustomClassLoader @ e76cbf7 
 *** CustomClassLoader finalisé! 

(La même chose est vraie avec Hashtable). Donc, dans le cas de log4j, ils n'ont pas eu de fuite de mémoire ni aucun type de bogue. Ils nettoyaient déjà la table de hachage et cela suffisait pour garantir le GC du chargeur de classe. IMO, le bogue est dans Tomcat, qui enregistre sans discernement ces erreurs "SÉVÈRES" à l'arrêt pour tous les ThreadLocals qui n'ont pas été explicitement .remove () d, qu'ils détiennent ou non une référence forte à une classe d'application. Il semble qu'au moins certains développeurs investissent du temps et des efforts pour "réparer" les fuites de mémoire fantôme sur le dire des journaux de Tomcat bâclés.

29
Neil Madden

Il n'y a rien de fondamentalement mauvais avec les sections locales de thread: elles ne provoquent pas de fuites de mémoire. Ils ne sont pas lents. Ils sont plus locaux que leurs homologues non-thread-local (c'est-à-dire qu'ils ont de meilleures propriétés de masquage des informations). Ils peuvent être mal utilisés, bien sûr, mais la plupart des autres outils de programmation le peuvent aussi…

Reportez-vous à cela lien par Joshua Bloch

1
MayurB

Une fuite de mémoire est provoquée lorsque ThreadLocal est toujours existant. Si l'objet ThreadLocal peut être GC, cela ne provoquera pas de fuite de mémoire. Parce que l'entrée dans ThreadLocalMap étend WeakReference, l'entrée sera GC après que l'objet ThreadLocal soit GC.

Le code ci-dessous crée beaucoup de ThreadLocal et il n'y a jamais de fuite de mémoire et le thread de main est toujours en direct.

// -XX:+PrintGCDetails -Xms100m -Xmx100m 
public class Test {

    public static long total = 1000000000;
    public static void main(String[] args) {
        for(long i = 0; i < total; i++) {
            // give GC some time
            if(i % 10000 == 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            ThreadLocal<Element> tl = new ThreadLocal<>();
            tl.set(new Element(i));
        }
    }
}

class Element {
    private long v;
    public Element(long v) {
        this.v = v;
    }
    public void finalize() {
        System.out.println(v);
    }
}
0
MaJinliang

Voici une alternative à ThreadLocal qui n'a pas de problème de fuite de mémoire:

class BetterThreadLocal<A> {
  Map<Thread, A> map = Collections.synchronizedMap(new WeakHashMap());

  A get() {
    ret map.get(Thread.currentThread());
  }

  void set(A a) {
    if (a == null)
      map.remove(Thread.currentThread());
    else
      map.put(Thread.currentThread(), a);
  }
}

Remarque: Il existe un nouveau scénario de fuite de mémoire, mais il est hautement improbable et peut être évité en suivant un simple guide. Le scénario conserve une référence forte à un objet Thread dans un BetterThreadLocal.

Je ne garde jamais de références fortes aux threads de toute façon parce que vous voulez toujours autoriser le thread à être GC'd lorsque son travail est terminé ... alors voilà: un ThreadLocal sans fuite de mémoire.

Quelqu'un devrait comparer cela. Je m'attends à ce qu'il soit à peu près aussi rapide que ThreadLocal de Java (les deux font essentiellement une recherche de carte de hachage faible, un seul recherche le thread, l'autre le ThreadLocal).

Exemple de programme en JavaX.

Et une note finale: Mon système ( JavaX ) garde également la trace de tous les WeakHashMaps et les nettoie régulièrement, de sorte que le dernier trou super-improbable est bouché (WeakHashMaps de longue durée qui ne sont jamais interrogés, mais quand même avoir des entrées périmées).

0
Stefan Reich

Les articles précédents expliquent le problème mais ne fournissent aucune solution. J'ai trouvé qu'il n'y avait aucun moyen de "vider" un ThreadLocal. Dans un environnement de conteneur où je gère des demandes, j'ai finalement simplement appelé .remove () à la fin de chaque demande. Je me rends compte que cela pourrait être problématique en utilisant des transactions gérées par conteneur.

0
Steve11235

Sous le code ,, l'instance t dans l'itération for ne peut pas être GC. Cela peut être un exemple de ThreadLocal & Memory Leak

public class MemoryLeak {

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++) {
                    TestClass t = new TestClass(i);
                    t.printId();
                    t = null;
                }
            }
        }).start();
    }


    static class TestClass{
        private int id;
        private int[] arr;
        private ThreadLocal<TestClass> threadLocal;
        TestClass(int id){
            this.id = id;
            arr = new int[1000000];
            threadLocal = new ThreadLocal<>();
            threadLocal.set(this);
        }

        public void printId(){
            System.out.println(threadLocal.get().id);
        }
    }
}
0
Yanhui Zhou