Fuite de mémoire Tomcat Guice / JDBC

J'ai une fuite de mémoire due à des fils orphelins dans Tomcat. En particulier, il semble que Guice et le pilote JDBC ne sont pas en train de fermer 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 c'est similaire à d'autres questions (comme celle-ci ), mais dans mon cas, la réponse de" ne vous inquiétez pas à ce sujet " ne sera pas suffisante, car il est à l'origine de problèmes pour moi. J'ai CI server qui met régulièrement à jour cette application, et après 6-10 recharges, le serveur CI il sera pendu parce que Tomcat est sorti de mémoire.

je dois être capable de nettoyer ces threads orphelins afin de pouvoir exécuter mon serveur CI de manière plus fiable. Toute aide serait appréciée!

48
demandé sur Community 2012-08-09 00:05:12

9 réponses

je viens de régler ce problème moi-même. Contrairement à d'autres réponses, Je ne recommande pas l'émission de la commande t.stop() . Cette méthode a été dépréciée, et pour de bonnes raisons. De référence Oracle des raisons de pour ce faire.

cependant, il existe une solution pour supprimer cette erreur sans devoir recourir à t.stop() ...

vous pouvez utiliser la plupart du code @Oso fourni, 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
        }
    }
}

remplacer par 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 correctement arrêter le thread, et l'erreur devrait disparaître.

50
répondu Bill 2015-05-26 13:31:35

j'ai eu le même problème, et comme Jeff le dit, l'approche" don't worry about it " n'était pas la bonne.

j'ai fait un Servlettcontextlistener qui stoppe le thread suspendu lorsque le contexte est fermé, et j'ai ensuite enregistré ce ContextListener sur le web.fichier xml.

je sais déjà que l'arrêt d'un thread n'est pas une façon élégante de traiter avec eux, mais autrement le serveur continue à s'écraser après deux ou trois déploiements (il n'est pas toujours possibilité de redémarrer le serveur app).

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, enregistrez-la sur le web.fichier xml:

<web-app...
    <listener>
        <listener-class>path.to.ContextFinalizer</listener-class>
    </listener>
</web-app>
15
répondu Oso 2016-11-04 03:51:24

la solution la moins invasive est de forcer l'initialisation du pilote JDBC MySQL à partir du code en dehors du classloader de la webapp.

dans tomcat/conf/server.xml, de les modifier (à l'intérieur du Serveur élément):

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

à

<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener"
          classesToInitialize="com.mysql.jdbc.NonRegisteringDriver" />

cela suppose que vous mettez le pilote MySQL JDBC dans le répertoire lib de tomcat et non dans votre webapp.la guerre du WEB-INF/lib, comme le point de l'ensemble est de charger le pilote avant et indépendamment de votre webapp.

, les Références:

13
répondu Stefan L 2014-01-29 09:09:24

à partir du connecteur MySQL 5.1.23, une méthode est fournie pour arrêter le fil nettoyant de connexion abandonné, AbandonedConnectionCleanupThread.shutdown .

Cependant, nous ne voulons pas de dépendances directes dans notre code sur le code du pilote JDBC par ailleurs opaque, donc ma solution est d'utiliser la réflexion pour trouver la classe et la méthode et de 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();
    }

ceci termine proprement le fil si le pilote JDBC est une version suffisamment récente du connecteur MySQL et ne fait rien d'autre.

notez qu'il doit être exécuté dans le contexte du chargeur de classe parce que 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é, alors le thread ne sera pas exécuté pour des interactions JDBC ultérieures.

11
répondu Lawrence Dol 2017-10-13 04:05:27

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 de L'Oso avec L'amélioration du pilote de Bill et L'amélioration de la réflexion du logiciel Monkey. (J'ai aimé la simplicité de la réponse de Stephan L aussi, mais parfois modifier L'environnement Tomcat lui-même n'est pas une bonne option, surtout si vous avez à traiter avec l'autoscaling ou la migration vers un autre conteneur web.)

au lieu de se référer directement pour le nom de la classe, le nom du thread et la méthode stop, je les ai aussi encapsulés dans une classe ThreadInfo interne privée. En utilisant une liste de ces objets ThreadInfo, vous pouvez inclure des threads gênants supplémentaires pour être arrêté avec le même code. C'est un peu plus complexe d'une solution que la plupart des gens ont probablement besoin, mais devrait fonctionner plus généralement quand vous avez besoin de cela.

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="/q/tomcat-guice-jdbc-memory-leak-10728/">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();
                        }
                    }
                }
            }
        }
    }

}
4
répondu Richard J. Barbalace 2014-01-06 22:27:48

je suis allé un peu plus loin de L'Oso, j'ai amélioré le code ci-dessus en deux points:

  1. a ajouté le thread Finalizer au contrôle à tuer:

    for(Thread t:threadArray) {
            if(t.getName().contains("Abandoned connection cleanup thread") 
                ||  t.getName().matches("com\.google.*Finalizer")
                ) {
            synchronized(t) {
                logger.warn("Forcibly stopping thread to avoid memory leak: " + t.getName());
                t.stop(); //don't complain, it works
            }
        }
    }
    
  2. dormir un peu pour donner aux fils le temps de s'arrêter. Sans ça, tomcat n'arrêtait pas de se plaindre.

    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        logger.debug(e.getMessage(), e);
    }
    
2
répondu BrunoJCM 2012-11-13 15:31:56

la solution de Bill semble bonne, cependant j'ai trouvé une autre solution directement dans les rapports de bogue MySQL:

[5 Juin 2013 17:12] Christopher Schultz Ici est une bien meilleure solution jusqu'à ce que quelque chose change.

Active Jremememoryleakpreventionlistener de Tomcat (activé par défaut sur Tomcat 7), et ajoute cet attribut à l'élément:

classesToInitialize= " com.mysql.jdbc.NonRegisteringDriver "

si "classesToInitialize" est déjà défini sur votre , il suffit d'ajouter NonRegisteringDriver à la valeur existante séparée par une virgule.

et la réponse:

[8 Jun 2013 21:33] Marko Asplund J'ai fait quelques tests avec le Jremememoryleakpreventionlistener / classesToInitialize workaround (Tomcat 7.0.39 + MySQL Connector / J 5.1.25).

avant d'appliquer le workaround thread dumps listed multiple Abandonedconnection Cleanupthread instances après avoir redéployé le webapp plusieurs fois. Après avoir appliqué la solution de contournement, il n'y a qu'une seule instance Abandonedconnection Cleanupthread.

j'ai dû modifier mon application, cependant, et déplacer le pilote MySQL de la webapp à Tomcat lib. Sinon, le classloader est incapable de charger COM.mysql.jdbc.Conducteur non enregistré au démarrage de Tomcat.

j'espère que cela aide pour tous ceux qui se battent encore avec ce problème...

2
répondu wmiki 2014-06-28 22:34:51

Voir Pour éviter une fuite de mémoire, le Pilote JDBC a été expulsé non . La réponse de Bill supprime toutes les instances de pilotes ainsi que les instances qui peuvent appartenir à d'autres applications web. J'ai étendu la réponse de Bill avec une vérification que L'instance du pilote appartient à la droite ClassLoader .

voici le code résultant (dans une méthode séparée, parce que mon contextDestroyed a d'autres choses à faire):

// See /q/the-web-application-appears-to-have-started-a-thread-named-abandoned-connection-cleanup-thread-com-mysql-jdbc-abandonedconnectioncleanupthread-27658/"Driver %s deregistered", d));
            }
            else {
                logger.info(String.format("Driver %s not deregistered because it might be in use elsewhere", d.toString()));
            }
        }
        catch (SQLException ex) {
            logger.warning(String.format("Error deregistering driver %s, exception: %s", d.toString(), ex.toString()));
        }
    }
    try {
         AbandonedConnectionCleanupThread.shutdown();
    }
    catch (InterruptedException e) {
        logger.warning("SEVERE problem cleaning up: " + e.getMessage());
        e.printStackTrace();
    }
}

je me demande si l'appel AbandonedConnectionCleanupThread.shutdown() est sans danger. Peut-il interférer avec d'autres applications web? J'espère que non, parce que la méthode AbandonedConnectionCleanupThread.run() n'est pas statique mais la méthode AbandonedConnectionCleanupThread.shutdown() l'est.

1
répondu Martijn Dirkse 2017-05-23 12:10:45

il semble que cela a été fixé dans 5.1.41 . Vous pouvez mettre à jour Connector/J à 5.1.41 ou plus récent. https://dev.mysql.com/doc/relnotes/connector-j/5.1/en/news-5-1-41.html

la mise en œuvre de Abandonedconnection Cleanupthread a maintenant été améliorée, de sorte qu'Il ya maintenant quatre façons pour les développeurs de faire face à la situation:

  • lorsque la valeur par défaut La configuration Tomcat est utilisée et le connecteur / j jar est placé dans un répertoire local de bibliothèque, le nouveau détecteur d'application intégré dans Connector/J détecte maintenant l'arrêt de l'application web en 5 secondes et tue Abandonedconnection Cleanupthread. Tout avertissement inutile sur le fait que le fil est imparable est également évité. Si le connecteur / j jar est placé dans un répertoire de bibliothèque global, le thread est laissé en cours d'exécution jusqu'à ce que la JVM soit déchargée.

  • lorsque le contexte de Tomcat est configuré avec l'attribut clearReferencesStopThreads=" true", Tomcat va arrêter tous les threads générés lorsque L'application s'arrête à moins que Connector/J ne soit partagé avec d'autres applications web, auquel cas Connector/J est maintenant protégé contre un arrêt inapproprié par Tomcat; L'avertissement au sujet du thread non-stoppable est toujours publié dans le journal des erreurs de Tomcat.

  • Lorsqu'un Servlettextlistener est mis en œuvre dans chaque application web qui appelle Abandonedconnection Cleanupthread.checkedShutdown () sur la destruction de contexte, Connector/J maintenant, encore une fois, saute cette opération si le pilote est potentiellement partagé avec d'autres applications. Aucun avertissement sur le fait que le thread est imparable n'est envoyé au journal des erreurs de Tomcat dans ce cas.

  • En Cas D'Abandon De La Connexion Cleanupthread.uncheckedShutdown () est appelé, le Abandonedconnection Cleanupthread est fermé même si Connector / J est partagé avec d'autres applications. Cependant, il ne sera peut-être pas possible de redémarrer le thread par la suite.

si vous regardez le code source, ils ont appelé setDeamon(true) sur thread, donc il ne va pas bloquer shutdown.

Thread t = new Thread(r, "Abandoned connection cleanup thread");
t.setDaemon(true);
1
répondu Le Zhang 2017-05-23 09:18:51