50 votes

Fuite de mémoire Tomcat Guice/JDBC

Je rencontre une fuite de mémoire due à des threads orphelins dans Tomcat. En particulier, il semble que Guice et le pilote JDBC ne ferment pas les threads.

Aug 8, 2012 4:09:19 PM org.apache.catalina.loader.WebappClassLoader clearReferencesThreads
SEVERE: A web application appears to have started a thread named [com.google.inject.internal.util.$Finalizer] but has failed to stop it. This is very likely to create a memory leak.
Aug 8, 2012 4:09:19 PM org.apache.catalina.loader.WebappClassLoader clearReferencesThreads
SEVERE: A web application appears to have started a thread named [Abandoned connection cleanup thread] but has failed to stop it. This is very likely to create a memory leak.

Je sais que cette question est similaire à d'autres questions (telles que celui-ci ), mais dans mon cas, la réponse "ne vous en faites pas" ne sera pas suffisante, car cela me pose des problèmes. J'ai un serveur CI qui met régulièrement à jour cette application, et après 6 à 10 rechargements, le serveur CI se bloque parce que Tomcat n'a plus de mémoire.

Je dois être en mesure d'éliminer ces fils orphelins afin de pouvoir faire fonctionner mon serveur CI de manière plus fiable. Toute aide serait appréciée !

0 votes

Vous êtes sûr que ce sont les causes de l'erreur OOM ? Le problème JDBC est résolu en tuant le thread à l'aide d'un écouteur de contexte sur l'événement de destruction du contexte ET en plaçant le pilote dans la bibliothèque de l'application afin que le chargement des classes soit effectué dans le contexte de l'application et non dans celui du conteneur.

0 votes

Merci. Je suis assez nouveau dans ce domaine, donc je ne suis pas du tout sûr que ce soit la cause de l'erreur OOM, mais c'est la seule note suspecte que j'obtiens dans le journal de Tomcat lorsque je redéploie cette application Web. Avez-vous des conseils pour trouver la source ou utiliser correctement le contextListener comme vous le recommandez ? Après une recherche rapide sur Google, je n'ai trouvé aucun tutoriel pertinent, mais je serais heureux de me documenter sur le sujet si vous pouviez m'indiquer la bonne direction.

1 votes

Pour décharger les pilotes en utilisant un contextlistener, consultez les réponses ici : stackoverflow.com/questions/3320400/

52voto

Bill Points 1556

Je viens moi-même de régler ce problème. Contrairement à d'autres réponses, je ne recommande pas d'émettre la t.stop() commande. Cette méthode a été dépréciée, et ce pour une bonne raison. Référence Les raisons d'Oracle pour avoir fait ça.

Cependant, il existe une solution pour supprimer cette erreur sans avoir besoin de recourir à t.stop() ...

Vous pouvez utiliser la plupart du code fourni par @Oso, il suffit de remplacer la section suivante

Set<Thread> threadSet = Thread.getAllStackTraces().keySet();
Thread[] threadArray = threadSet.toArray(new Thread[threadSet.size()]);
for(Thread t:threadArray) {
    if(t.getName().contains("Abandoned connection cleanup thread")) {
        synchronized(t) {
            t.stop(); //don't complain, it works
        }
    }
}

Remplacez-le en utilisant la méthode suivante fournie par le pilote MySQL :

try {
    AbandonedConnectionCleanupThread.shutdown();
} catch (InterruptedException e) {
    logger.warn("SEVERE problem cleaning up: " + e.getMessage());
    e.printStackTrace();
}

Cela devrait permettre de fermer correctement le fil, et l'erreur devrait disparaître.

1 votes

Au début, c'était un peu difficile à trouver. Pas de javadocs, peu de références. Je l'ai découvert en regardant les rapports de bogues sur tomcat. Voici une référence indirecte d'oracle lui-même. docs.oracle.com/cd/E17952_01/connector-j-relnotes-fr/

2 votes

Ah génial, j'étais en 5.1.22. La classe a été introduite dans la 5.1.23. +1 Merci

1 votes

(désolé, les commentaires ne sont pas bien formatés...) Le lien que vous aviez sur "les raisons d'Oracle" devrait être mis à jour pour : docs.oracle.com/javase/6/docs/technotes/guides/concurrency/ Je voudrais ajouter une suggestion sur l'endroit où placer ceci. En utilisant Spring, j'ai ceci dans mon web.xml <listener> <listener-class>com.mypackage.web.context.MyContextLoaderListener</listener-class> </listener> Cette classe étend org.springframework.web.context.ContextLoaderListener et sur contextDestroyed() je fais cela.

15voto

Oso Points 355

J'ai eu le même problème, et comme le dit Jeff, l'approche "ne pas s'en faire" n'était pas la bonne.

J'ai créé un ServletContextListener qui arrête le thread suspendu lorsque le contexte est fermé, puis j'ai enregistré ce ContextListener dans le fichier web.xml.

Je sais déjà que l'arrêt d'un thread n'est pas une manière élégante de les traiter, mais sinon le serveur continue de planter après deux ou trois déploiements (il n'est pas toujours possible de redémarrer le serveur d'applications).

La classe que j'ai créée est :

public class ContextFinalizer implements ServletContextListener {

    private static final Logger LOGGER = LoggerFactory.getLogger(ContextFinalizer.class);

    @Override
    public void contextInitialized(ServletContextEvent sce) {
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        Enumeration<Driver> drivers = DriverManager.getDrivers();
        Driver d = null;
        while(drivers.hasMoreElements()) {
            try {
                d = drivers.nextElement();
                DriverManager.deregisterDriver(d);
                LOGGER.warn(String.format("Driver %s deregistered", d));
            } catch (SQLException ex) {
                LOGGER.warn(String.format("Error deregistering driver %s", d), ex);
            }
        }
        Set<Thread> threadSet = Thread.getAllStackTraces().keySet();
        Thread[] threadArray = threadSet.toArray(new Thread[threadSet.size()]);
        for(Thread t:threadArray) {
            if(t.getName().contains("Abandoned connection cleanup thread")) {
                synchronized(t) {
                    t.stop(); //don't complain, it works
                }
            }
        }
    }

}

Après avoir créé la classe, il faut l'enregistrer dans le fichier web.xml :

<web-app...
    <listener>
        <listener-class>path.to.ContextFinalizer</listener-class>
    </listener>
</web-app>

0 votes

C'est une mauvaise solution pour trois raisons. 1. Elle utilise t.stop(), qui est non seulement déprécié, mais dont la suppression est prévue. 2. Elle dépend d'une chaîne de programme arbitraire qui pourrait changer ("Abandoned connection cleanup thread"). 3. Il se synchronise sur un Thread ce qui pourrait entraîner des blocages surprenants.

14voto

Stefan L Points 722

La solution de contournement la moins invasive consiste à forcer l'initialisation du pilote MySQL JDBC à partir d'un code extérieur au classloader de la webapp.

Dans tomcat/conf/server.xml, modifiez (à l'intérieur de l'élément Server) :

<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />

a

<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener"
          classesToInitialize="com.mysql.jdbc.NonRegisteringDriver" />
  • Avec mysql-connector-java-8.0.x utilisez com.mysql.cj.jdbc.NonRegisteringDriver au lieu de

Cela suppose que vous avez placé le pilote MySQL JDBC dans le répertoire lib de tomcat et non dans le répertoire WEB-INF/lib de votre webapp.war, car le but est de charger le pilote. avant et indépendamment de votre webapp.

Références :

0 votes

Malheureusement, cela n'a pas fonctionné pour moi. J'utilise TomEE 1.6.0 (Tomcat 7.0.47) et le pilote MySQL 5.1.27. Finalement, j'ai choisi la solution basée sur la classe AbandonedConnectionCleanupThread. Le Thread killing a également fonctionné pour moi mais je préfère la solution qui est directement connectée à la source du problème qui est le pilote MySQL JDBC lui-même.

0 votes

@MiklosKrivan vous avez bien placé le fichier .jar de votre pilote JDBC dans le répertoire lib de tomcat et non dans le WEB-INF/lib de votre .war, n'est-ce pas ? Je pense que la première approche est nécessaire pour que le JreMemoryLeakPreventionListener fonctionne, et je suppose que la dernière approche est nécessaire pour relancer le thread AbandonedConnectionCleanupThread après avoir été arrêté une fois (il est lancé à partir d'un initialisateur statique dans le pilote JDBC lui-même).

0 votes

Oui, je place toujours le pilote JDBC dans la librairie de Tomcat car j'utilise des sources de données et je préfère que le conteneur gère le pool de bases de données et non l'application. Cela est nécessaire pour les serveurs d'applications Java d'entreprise, ce que j'utilise habituellement. Votre suggestion était liée à la situation où le pilote JDBC est dans la bibliothèque de Tomcat. C'est pourquoi j'ai écrit mon commentaire. Peut-être ai-je mal compris quelque chose ?

11voto

Lawrence Dol Points 27976

À partir du connecteur MySQL 5.1.23, une méthode est fournie pour arrêter le fil de nettoyage des connexions abandonnées, AbandonedConnectionCleanupThread.shutdown .

Cependant, nous ne voulons pas que notre code dépende directement du code opaque du pilote JDBC. Ma solution consiste donc à utiliser la réflexion pour trouver la classe et la méthode et l'invoquer si elle est trouvée. L'extrait de code complet suivant est tout ce qui est nécessaire, exécuté dans le contexte du chargeur de classe qui a chargé le pilote JDBC :

try {
    Class<?> cls=Class.forName("com.mysql.jdbc.AbandonedConnectionCleanupThread");
    Method   mth=(cls==null ? null : cls.getMethod("shutdown"));
    if(mth!=null) { mth.invoke(null); }
    }
catch (Throwable thr) {
    thr.printStackTrace();
    }

Cela termine proprement le fil de discussion si le pilote JDBC est une version suffisamment récente du connecteur MySQL et sinon ne fait rien.

Notez qu'il doit être exécuté dans le contexte du chargeur de classe car le thread est une référence statique ; si la classe du pilote n'est pas ou n'a pas déjà été déchargée lorsque ce code est exécuté, le thread ne sera pas exécuté pour les interactions JDBC suivantes.

2 votes

Je trouve que c'est la meilleure réponse de toutes pour deux raisons : 1) elle n'utilise pas Thread.stop() et 2) elle ne nécessite pas une dépendance explicite de l'application au connecteur MySQL.

6voto

J'ai pris les meilleures parties des réponses ci-dessus et les ai combinées dans une classe facilement extensible. Cela combine la suggestion originale d'Oso avec l'amélioration du pilote de Bill et l'amélioration de la réflexion de Software Monkey. (J'ai aussi aimé la simplicité de la réponse de Stephan L., mais parfois modifier l'environnement Tomcat lui-même n'est pas une bonne option, surtout si vous devez vous occuper de l'autoscaling ou de la migration vers un autre conteneur web).

Au lieu de faire directement référence au nom de la classe, au nom du fil et à la méthode d'arrêt, je les ai également encapsulés dans une classe interne privée ThreadInfo. En utilisant une liste de ces objets ThreadInfo, vous pouvez inclure d'autres threads gênants à arrêter avec le même code. Il s'agit d'une solution un peu plus complexe que ce dont la plupart des gens ont besoin, mais elle devrait fonctionner plus généralement lorsque vous en avez besoin.

import java.lang.reflect.Method;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.Set;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Context finalization to close threads (MySQL memory leak prevention).
 * This solution combines the best techniques described in the linked Stack
 * Overflow answer.
 * @see <a href="https://stackoverflow.com/questions/11872316/tomcat-guice-jdbc-memory-leak">Tomcat Guice/JDBC Memory Leak</a>
 */
public class ContextFinalizer
    implements ServletContextListener {

    private static final Logger LOGGER =
        LoggerFactory.getLogger(ContextFinalizer.class);

    /**
     * Information for cleaning up a thread.
     */
    private class ThreadInfo {

        /**
         * Name of the thread's initiating class.
         */
        private final String name;

        /**
         * Cue identifying the thread.
         */
        private final String cue;

        /**
         * Name of the method to stop the thread.
         */
        private final String stop;

        /**
         * Basic constructor.
         * @param n Name of the thread's initiating class.
         * @param c Cue identifying the thread.
         * @param s Name of the method to stop the thread.
         */
        ThreadInfo(final String n, final String c, final String s) {
            this.name = n;
            this.cue  = c;
            this.stop = s;
        }

        /**
         * @return the name
         */
        public String getName() {
            return this.name;
        }

        /**
         * @return the cue
         */
        public String getCue() {
            return this.cue;
        }

        /**
         * @return the stop
         */
        public String getStop() {
            return this.stop;
        }
    }

    /**
     * List of information on threads required to stop.  This list may be
     * expanded as necessary.
     */
    private List<ThreadInfo> threads = Arrays.asList(
        // Special cleanup for MySQL JDBC Connector.
        new ThreadInfo(
            "com.mysql.jdbc.AbandonedConnectionCleanupThread", //$NON-NLS-1$
            "Abandoned connection cleanup thread", //$NON-NLS-1$
            "shutdown" //$NON-NLS-1$
        )
    );

    @Override
    public void contextInitialized(final ServletContextEvent sce) {
        // No-op.
    }

    @Override
    public final void contextDestroyed(final ServletContextEvent sce) {

        // Deregister all drivers.
        Enumeration<Driver> drivers = DriverManager.getDrivers();
        while (drivers.hasMoreElements()) {
            Driver d = drivers.nextElement();
            try {
                DriverManager.deregisterDriver(d);
                LOGGER.info(
                    String.format(
                        "Driver %s deregistered", //$NON-NLS-1$
                        d
                    )
                );
            } catch (SQLException e) {
                LOGGER.warn(
                    String.format(
                        "Failed to deregister driver %s", //$NON-NLS-1$
                        d
                    ),
                    e
                );
            }
        }

        // Handle remaining threads.
        Set<Thread> threadSet = Thread.getAllStackTraces().keySet();
        Thread[] threadArray = threadSet.toArray(new Thread[threadSet.size()]);
        for (Thread t:threadArray) {
            for (ThreadInfo i:this.threads) {
                if (t.getName().contains(i.getCue())) {
                    synchronized (t) {
                        try {
                            Class<?> cls = Class.forName(i.getName());
                            if (cls != null) {
                                Method mth = cls.getMethod(i.getStop());
                                if (mth != null) {
                                    mth.invoke(null);
                                    LOGGER.info(
                                        String.format(
            "Connection cleanup thread %s shutdown successfully.", //$NON-NLS-1$
                                            i.getName()
                                        )
                                    );
                                }
                            }
                        } catch (Throwable thr) {
                            LOGGER.warn(
                                    String.format(
            "Failed to shutdown connection cleanup thread %s: ", //$NON-NLS-1$
                                        i.getName(),
                                        thr.getMessage()
                                    )
                                );
                            thr.printStackTrace();
                        }
                    }
                }
            }
        }
    }

}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X