Fuite de mémoire Tomcat Guice / JDBC

Je suis confronté à une fuite de mémoire due à des threads orphelins dans Tomcat. En particulier, il semble que Guice et le pilote JDBC ne ferment pas de 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 cela ressemble à d’autres questions (comme celle-ci ), mais dans mon cas, la réponse «ne vous inquiétez pas» ne suffira pas, car cela me cause des problèmes. J’ai un serveur CI qui met régulièrement à jour cette application, et après 6-10 recharges, le serveur CI se bloque car Tomcat manque de mémoire.

Je dois pouvoir effacer ces threads orphelins pour pouvoir exécuter mon serveur CI de manière plus fiable. Toute aide serait appréciée!

Je viens de régler ce problème moi-même. Contrairement à d’autres réponses, je ne recommande pas d’émettre la commande t.stop() . Cette méthode est obsolète et pour une bonne raison. Référence à Oracle pour ce faire.

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

Vous pouvez utiliser la plupart du code @Oso fourni, remplacez simplement la section suivante

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

J’ai eu le même problème et, comme le dit Jeff, l’approche «ne vous inquiétez pas» n’était pas la solution.

J’ai fait un ServletContextListener qui arrête le thread bloqué lors de la fermeture du contexte, puis enregistre ce ContextListener sur 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 gérer, mais sinon le serveur continue à 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 drivers = DriverManager.getDrivers(); Driver d = null; while(drivers.hasMoreElements()) { try { d = drivers.nextElement(); DriverManager.deregisterDriver(d); LOGGER.warn(Ssortingng.format("Driver %s deregistered", d)); } catch (SQLException ex) { LOGGER.warn(Ssortingng.format("Error deregistering driver %s", d), ex); } } Set 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 fichier web.xml:

  path.to.ContextFinalizer   

La solution la moins invasive consiste à forcer l’initialisation du pilote JDBC MySQL à partir du code situé en dehors du chargeur de classes de l’application Web.

Dans tomcat / conf / server.xml, modifiez (dans l’élément Server):

  

à

  

Cela suppose que vous placiez le pilote JDBC MySQL dans le répertoire lib de tomcat et non dans le répertoire WEB-INF / lib de webapp.war, le but étant de charger le pilote avant et indépendamment de votre application Web.

Les références:

À partir du connecteur MySQL 5.1.23, une méthode est fournie pour fermer le thread de nettoyage de connexion abandonné, AbandonedConnectionCleanupThread.shutdown .

Cependant, nous ne voulons pas de dépendances directes dans notre code sur le code de pilote JDBC par ailleurs opaque. Ma solution est donc d’utiliser la reflection 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 thread si le pilote JDBC est une version suffisamment récente du connecteur MySQL et ne fait rien autrement.

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

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 reflection de Software Monkey. (J’ai aimé la simplicité de la réponse de Stephan L, mais parfois, modifier l’environnement Tomcat n’est pas une bonne option, surtout si vous devez gérer la mise à l’échelle automatique ou la migration vers un autre conteneur Web.)

Au lieu de faire directement référence au nom de la classe, au nom du thread et à la méthode d’arrêt, je les ai également encapsulés dans une classe ThreadInfo interne privée. En utilisant une liste de ces objects ThreadInfo, vous pouvez inclure d’autres threads problématiques à arrêter avec le même code. Cette solution est un peu plus complexe que ce que la plupart des gens ont probablement besoin, mais 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 Tomcat Guice/JDBC Memory Leak */ 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 Ssortingng name; /** * Cue identifying the thread. */ private final Ssortingng cue; /** * Name of the method to stop the thread. */ private final Ssortingng 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 Ssortingng n, final Ssortingng c, final Ssortingng s) { this.name = n; this.cue = c; this.stop = s; } /** * @return the name */ public Ssortingng getName() { return this.name; } /** * @return the cue */ public Ssortingng getCue() { return this.cue; } /** * @return the stop */ public Ssortingng getStop() { return this.stop; } } /** * List of information on threads required to stop. This list may be * expanded as necessary. */ private List 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 drivers = DriverManager.getDrivers(); while (drivers.hasMoreElements()) { Driver d = drivers.nextElement(); try { DriverManager.deregisterDriver(d); LOGGER.info( Ssortingng.format( "Driver %s deregistered", //$NON-NLS-1$ d ) ); } catch (SQLException e) { LOGGER.warn( Ssortingng.format( "Failed to deregister driver %s", //$NON-NLS-1$ d ), e ); } } // Handle remaining threads. Set 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( Ssortingng.format( "Connection cleanup thread %s shutdown successfully.", //$NON-NLS-1$ i.getName() ) ); } } } catch (Throwable thr) { LOGGER.warn( Ssortingng.format( "Failed to shutdown connection cleanup thread %s: ", //$NON-NLS-1$ i.getName(), thr.getMessage() ) ); thr.printStackTrace(); } } } } } } } 

Je suis allé plus loin que Oso, j’ai amélioré le code ci-dessus en deux points:

  1. Ajout du thread Finalizer au test de nécessité de 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. Dormez un peu pour laisser le temps aux fils de s’arrêter. Sans cela, Tomcat continuait à se plaindre.

     try { Thread.sleep(1000); } catch (InterruptedException e) { logger.debug(e.getMessage(), e); } 

La solution de Bill a l’air bien, mais j’ai trouvé une autre solution directement dans les rapports de bogues de MySQL:

[5 juin 2013 17:12] Christopher Schultz Voici une solution beaucoup plus efficace jusqu’à ce que quelque chose d’autre change.

Activez JreMemoryLeakPreventionListener de Tomcat (activé par défaut sur Tomcat 7) et ajoutez cet atsortingbut à l’élément:

classesToInitialize = “com.mysql.jdbc.NonRegisteringDriver”

Si “classesToInitialize” est déjà défini sur votre, ajoutez simplement NonRegisteringDriver à la valeur existante séparée par une virgule.

et la réponse:

[8 juin 2013 21:33] Marko Asplund J’ai fait des tests avec la solution JreMemoryLeakPreventionListener / classesToInitialize (Tomcat 7.0.39 + MySQL Connector / J 5.1.25).

Avant d’appliquer la solution de contournement, les vidages de thread répertorient plusieurs instances d’AbandonedConnectionCleanupThread après avoir redéployé l’application Web plusieurs fois. Après avoir appliqué la solution de contournement, il n’y a qu’une seule instance AbandonedConnectionCleanupThread.

J’ai dû modifier mon application, cependant, et déplacer le pilote MySQL de l’application Web vers la librairie Tomcat. Sinon, le chargeur de classe ne peut pas charger com.mysql.jdbc.NonRegisteringDriver au démarrage de Tomcat.

J’espère que cela aidera tous ceux qui luttent encore contre ce problème …

Voir Pour éviter une fuite de mémoire, le pilote JDBC a été désenregistré de force . La réponse de Bill désenregistre toutes les instances de pilotes ainsi que les instances pouvant appartenir à d’autres applications Web. J’ai étendu la réponse de Bill en vérifiant que l’instance du pilote appartient au ClassLoader .

Voici le code résultant (dans une méthode distincte, car mon contextDestroyed a d’autres choses à faire):

 // See https://stackoverflow.com/questions/25699985/the-web-application-appears-to-have-started-a-thread-named-abandoned-connect // and // https://stackoverflow.com/questions/3320400/to-prevent-a-memory-leak-the-jdbc-driver-has-been-forcibly-unregistered/23912257#23912257 private void avoidGarbageCollectionWarning() { ClassLoader cl = Thread.currentThread().getContextClassLoader(); Enumeration drivers = DriverManager.getDrivers(); Driver d = null; while (drivers.hasMoreElements()) { try { d = drivers.nextElement(); if(d.getClass().getClassLoader() == cl) { DriverManager.deregisterDriver(d); logger.info(Ssortingng.format("Driver %s deregistered", d)); } else { logger.info(Ssortingng.format("Driver %s not deregistered because it might be in use elsewhere", d.toSsortingng())); } } catch (SQLException ex) { logger.warning(Ssortingng.format("Error deregistering driver %s, exception: %s", d.toSsortingng(), ex.toSsortingng())); } } 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 sûr. Peut-il interférer avec d’autres applications Web? J’espère que non, car la méthode AbandonedConnectionCleanupThread.run() n’est pas statique mais la méthode AbandonedConnectionCleanupThread.shutdown() est.

Il semble que cela ait été corrigé en 5.1.41 . Vous pouvez mettre à niveau Connector / J vers 5.1.41 ou plus récent. https://dev.mysql.com/doc/relnotes/connector-j/5.1/en/news-5-1-41.html

L’implémentation de AbandonedConnectionCleanupThread a maintenant été améliorée, de sorte qu’il y a maintenant quatre façons pour les développeurs de gérer la situation:

  • Lorsque la configuration Tomcat par défaut est utilisée et que Connector / J jar est placé dans un répertoire de bibliothèque local, le nouveau détecteur d’application intégré dans Connector / J détecte maintenant l’arrêt de l’application Web dans les 5 secondes et détruit AbandonedConnectionCleanupThread. Tous les avertissements inutiles sur le thread étant imparable sont également évités. Si le Connector / J jar est placé dans un répertoire de bibliothèque global, le thread rest actif jusqu’à ce que la JVM soit déchargée.

  • Lorsque le contexte de Tomcat est configuré avec l’atsortingbut clearReferencesStopThreads = “true”, Tomcat va arrêter tous les threads générés lorsque l’application s’arrête, à moins que Connector / J soit partagé avec d’autres applications Web, auquel cas Connector / J est désormais protégé contre arrêter par Tomcat; l’avertissement concernant le thread non-bloquable est toujours émis dans le journal des erreurs de Tomcat.

  • Lorsqu’un ServletContextListener est implémenté dans chaque application Web qui appelle AbandonedConnectionCleanupThread.checkedShutdown () lors de la destruction du contexte, Connector / J ignore à nouveau cette opération si le pilote est potentiellement partagé avec d’autres applications. Dans ce cas, aucun avertissement concernant le thread ne pouvant être arrêté est envoyé au journal des erreurs de Tomcat.

  • Lorsque AbandonedConnectionCleanupThread.uncheckedShutdown () est appelé, AbandonedConnectionCleanupThread 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 le thread, il ne bloquera donc pas l’arrêt.

 Thread t = new Thread(r, "Abandoned connection cleanup thread"); t.setDaemon(true);