Tomcat Guice / JDBC Memory Leak

Estoy experimentando una pérdida de memoria debido a hilos huérfanos en Tomcat. En particular, parece que Guice y el controlador JDBC no están cerrando hilos.

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. 

Sé que esto es similar a otras preguntas (como esta ), pero en mi caso, la respuesta de “no te preocupes” no será suficiente, ya que me está causando problemas. Tengo un servidor de CI que regularmente actualiza esta aplicación, y después de 6-10 recargas, el servidor de CI se cuelga porque Tomcat está sin memoria.

Necesito poder borrar estos hilos huérfanos para que pueda ejecutar mi servidor de CI de manera más confiable. ¡Cualquier ayuda sería apreciada!

Acabo de resolver este problema yo mismo. Contrariamente a algunas otras respuestas, no recomiendo emitir el t.stop() . Este método ha sido desaprobado, y por buenas razones. Refiérase a las razones de Oracle para hacer esto.

Sin embargo, hay una solución para eliminar este error sin necesidad de recurrir a t.stop()

Puede usar la mayor parte del código @Oso proporcionado, simplemente reemplace la siguiente sección

 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 } } } 

Reemplácela usando el siguiente método provisto por el controlador MySQL:

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

Esto debería apagar correctamente el hilo, y el error debería desaparecer.

He tenido el mismo problema, y ​​como dice Jeff, el enfoque “no te preocupes por eso” no era el camino a seguir.

Hice un ServletContextListener que detiene el hilo colgado cuando se cierra el contexto, y luego registró dicho ContextListener en el archivo web.xml.

Ya sé que detener un hilo no es una forma elegante de lidiar con ellos, pero de lo contrario el servidor sigue bloqueándose después de dos o tres implementaciones (no siempre es posible reiniciar el servidor de aplicaciones).

La clase que creé es:

 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(String.format("Driver %s deregistered", d)); } catch (SQLException ex) { LOGGER.warn(String.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 } } } } } 

Después de crear la clase, regístrela en el archivo web.xml:

  path.to.ContextFinalizer   

La solución menos invasiva es forzar la inicialización del controlador JDBC de MySQL del código fuera del cargador de clases de la aplicación web.

En tomcat / conf / server.xml, modify (dentro del elemento Servidor):

  

a

  

Esto supone que coloque el controlador JDBC de MySQL en el directorio lib de tomcat y no dentro del directorio WEB-INF / lib de su webapp.war, ya que el objective es cargar el controlador antes e independientemente de su aplicación web.

Referencias

A partir del conector de MySQL 5.1.23 en adelante, se proporciona un método para cerrar el hilo de limpieza de conexión abandonado, AbandonedConnectionCleanupThread.shutdown .

Sin embargo, no deseamos dependencias directas en nuestro código en el código de controlador JDBC opaco, por lo que mi solución es usar la reflexión para encontrar la clase y el método e invocarlo si se encuentra. El siguiente fragmento de código completo es todo lo que se necesita, se ejecuta en el contexto del cargador de clases que cargó el controlador 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(); } 

Esto termina limpiamente el hilo si el controlador JDBC es una versión suficientemente reciente del conector MySQL y de lo contrario no hace nada.

Tenga en cuenta que debe ejecutarse en el contexto del cargador de clases porque el hilo es una referencia estática; si la clase de controlador no se está descargando o no se ha descargado cuando se ejecuta este código, el hilo no se ejecutará para las interacciones de JDBC subsiguientes.

Tomé las mejores partes de las respuestas anteriores y las combiné en una clase fácilmente extensible. Esto combina la sugerencia original de Oso con la mejora del controlador de Bill y la mejora de la reflexión de Software Monkey. (Me gustó la simplicidad de la respuesta de Stephan L’también, pero a veces la modificación del entorno de Tomcat en sí no es una buena opción, especialmente si tiene que lidiar con la autoescala o la migración a otro contenedor web).

En lugar de referirme directamente al nombre de la clase, el nombre de la secuencia y el método de detención, también los encapsulé en una clase ThreadInfo interna privada. Usando una lista de estos objetos ThreadInfo, puede incluir hilos problemáticos adicionales para cerrar con el mismo código. Esta es una solución un poco más compleja de lo que la mayoría de la gente probablemente necesita, pero debería funcionar de manera más general cuando la necesite.

 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 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 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( 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 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(); } } } } } } } 

Fui un paso más allá de Oso, mejoré el código anterior en dos puntos:

  1. Se agregó el hilo de Finalizer a la verificación de necesidad de matar:

     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. Duerme un rato para dejar que los hilos se detengan. Sin eso, Tomcat siguió quejándose.

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

La solución de Bill se ve bien, sin embargo, encontré otra solución directamente en los informes de errores de MySQL:

[5 de junio 2013 17:12] Christopher Schultz Aquí hay una solución mucho mejor hasta que algo más cambie.

Active el JREMemoryLeakPreventionListener de Tomcat (habilitado por defecto en Tomcat 7) y agregue este atributo al elemento:

classesToInitialize = “com.mysql.jdbc.NonRegisteringDriver”

Si “classesToInitialize” ya está configurado en su, simplemente agregue NonRegisteringDriver al valor existente separado por una coma.

y la respuesta:

[8 Jun 2013 21:33] Marko Asplund Hice algunas pruebas con la solución alternativa JreMemoryLeakPreventionListener / classesToInitialize (Tomcat 7.0.39 + MySQL Connector / J 5.1.25).

Antes de aplicar los volcados de subproceso de la cadena de problemas enumeraba varias instancias de AbandonedConnectionCleanupThread después de volver a implementar la aplicación web varias veces. Después de aplicar la solución, solo hay una instancia de AbandonedConnectionCleanupThread.

Sin embargo, tuve que modificar mi aplicación y mover el controlador MySQL de la aplicación web a la lib de Tomcat. De lo contrario, el cargador de clases no puede cargar com.mysql.jdbc.NonRegisteringDriver en el inicio de Tomcat.

Espero que ayude a todos los que aún luchan con este problema …

Consulte Para evitar una pérdida de memoria, el controlador JDBC no se ha registrado por la fuerza . La respuesta de Bill cancela todas las instancias de Driver, así como las instancias que pueden pertenecer a otras aplicaciones web. He extendido la respuesta de Bill con una verificación de que la instancia de Driver pertenece al ClassLoader correcto.

Aquí está el código resultante (en un método diferente, porque mi contextDestroyed tiene otras cosas que hacer):

 // 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(String.format("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(); } } 

Me pregunto si la llamada AbandonedConnectionCleanupThread.shutdown() es segura. ¿Puede interferir con otras aplicaciones web? Espero que no, porque el método AbandonedConnectionCleanupThread.run() no es estático, pero el método AbandonedConnectionCleanupThread.shutdown() es.

Parece que esto fue arreglado en 5.1.41 . Puede actualizar Connector / J a 5.1.41 o posterior. https://dev.mysql.com/doc/relnotes/connector-j/5.1/en/news-5-1-41.html

La implementación de AbandonedConnectionCleanupThread ahora se ha mejorado, por lo que ahora hay cuatro formas en que los desarrolladores pueden manejar la situación:

  • Cuando se usa la configuración predeterminada de Tomcat y el jalón de Connector / J se coloca en un directorio de biblioteca local, el nuevo detector de aplicaciones incorporado en Connector / J ahora detecta la detención de la aplicación web en 5 segundos y mata a AbandonedConnectionCleanupThread. También se evitan las advertencias innecesarias sobre que el hilo es imparable. Si Connector / J jar se coloca en un directorio de biblioteca global, el subproceso se deja en ejecución hasta que se descarga la JVM.

  • Cuando el contexto de Tomcat se configura con el atributo clearReferencesStopThreads = “true”, Tomcat detendrá todos los hilos generados cuando la aplicación se detenga a menos que Connector / J se comparta con otras aplicaciones web, en cuyo caso Connector / J ahora está protegido contra un inadecuado pasa por Tomcat; la advertencia sobre el hilo irreprochable se sigue emitiendo en el registro de errores de Tomcat.

  • Cuando se implementa un ServletContextListener dentro de cada aplicación web que llama a AbandonedConnectionCleanupThread.checkedShutdown () en la destrucción del contexto, Connector / J ahora, de nuevo, omite esta operación si el controlador se comparte potencialmente con otras aplicaciones. En este caso, no se emite ninguna advertencia acerca de que el hilo sea imparable en el registro de errores de Tomcat.

  • Cuando se llama a AbandonedConnectionCleanupThread.uncheckedShutdown (), AbandonedConnectionCleanupThread se cierra incluso si Connector / J se comparte con otras aplicaciones. Sin embargo, puede que no sea posible reiniciar el hilo después.

Si nos fijamos en el código fuente, llamaron a setDeamon (verdadero) en el hilo, por lo que no bloqueará el cierre.

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