Para evitar una pérdida de memoria, el controlador JDBC no se ha registrado por la fuerza

325

Recibo este mensaje cuando ejecuto mi aplicación web. Funciona bien pero recibo este mensaje durante el apagado.

GRAVE: Una aplicación web registró el controlador JBDC [oracle.jdbc.driver.OracleDriver] pero no pudo cancelar el registro cuando se detuvo la aplicación web. Para evitar una pérdida de memoria, el controlador JDBC no se ha registrado por la fuerza.

Cualquier ayuda apreciada.

Mona
fuente
44
Podría ser un duplicado de stackoverflow.com/questions/2604630/…
skaffman el

Respuestas:

301

Desde la versión 6.0.24, Tomcat se entrega con una función de detección de pérdida de memoria , que a su vez puede generar este tipo de mensajes de advertencia cuando hay un controlador compatible con JDBC 4.0 en la aplicación web /WEB-INF/libque se registra automáticamente durante el inicio de la aplicación utilizando la ServiceLoaderAPI , pero que no se dio de baja automáticamente durante el cierre de la aplicación web. Este mensaje es puramente informal, Tomcat ya ha tomado la acción de prevención de pérdida de memoria en consecuencia.

¿Qué puedes hacer?

  1. Ignora esas advertencias. Tomcat está haciendo bien su trabajo. El error real está en el código de otra persona (el controlador JDBC en cuestión), no en el tuyo. Esté feliz de que Tomcat haya hecho su trabajo correctamente y espere hasta que el proveedor del controlador JDBC lo repare para que pueda actualizar el controlador. Por otro lado, se supone que no debe soltar un controlador JDBC en aplicaciones web /WEB-INF/lib, sino solo en servidores /lib. Si aún lo mantiene en la aplicación web /WEB-INF/lib, debe registrarlo y cancelar su registro manualmente mediante a ServletContextListener.

  2. Cambie a Tomcat 6.0.23 o anterior para que no le molesten esas advertencias. Pero en silencio seguirá perdiendo memoria. No estoy seguro si es bueno saberlo después de todo. Ese tipo de pérdidas de memoria son una de las principales causas de OutOfMemoryErrorproblemas durante las implementaciones en caliente de Tomcat.

  3. Mueva el controlador JDBC a la /libcarpeta de Tomcat y tenga una fuente de datos agrupada de conexión para administrar el controlador. Tenga en cuenta que el DBCP incorporado de Tomcat no anula el registro de los controladores correctamente al cerrar. Vea también el error DBCP-322 que está cerrado como WONTFIX. Prefiere reemplazar DBCP por otro grupo de conexiones que esté haciendo su trabajo mejor que DBCP. Por ejemplo , HikariCP , BoneCP o quizás Tomcat JDBC Pool .

BalusC
fuente
25
Este es un buen consejo. No es una advertencia de una fuga de memoria, es una advertencia de que Tomcat tomó alguna acción forzada para evitar una fuga
mate b
49
Si la opción (1) es el camino a seguir, ¿por qué Tomcat registra esto como GRAVE? SEVERE para mí significa "página del administrador", no "ignorar".
Peter Becker
77
¿Por qué no hacerlo usted mismo? En lugar de esperar que Tomcat lo haga. En mi opinión, no es el trabajo de Tomcat limpiar nuestro código desordenado. Vea mi respuesta a continuación.
sparkyspider
2
@sproketboy: ¿Eh? ¿Estaba asignando artefactos JDBC como campos de una clase que a su vez se almacena en la sesión HTTP?
BalusC
2
Supongo que generalmente es la razón 3 (tener la biblioteca en la wAR en lugar de lib).
lapo
160

En su método servlet context listener contextDestroyed (), anule el registro manualmente de los controladores:

// This manually deregisters JDBC driver, which prevents Tomcat 7 from complaining about memory leaks wrto this class
Enumeration<Driver> drivers = DriverManager.getDrivers();
while (drivers.hasMoreElements()) {
    Driver driver = drivers.nextElement();
    try {
        DriverManager.deregisterDriver(driver);
        LOG.log(Level.INFO, String.format("deregistering jdbc driver: %s", driver));
    } catch (SQLException e) {
        LOG.log(Level.SEVERE, String.format("Error deregistering driver %s", driver), e);
    }
}
ae6rt
fuente
3
¡Funciona! javabeat.net/servletcontextlistener-example puede ayudar a implementar el escucha de contexto de servlet
Vadim Zin4uk
17
Esto es potencialmente inseguro en un entorno compartido, ya que es posible que no desee cancelar el registro de todos los controladores JDBC que están disponibles. Vea mi respuesta para un enfoque más seguro.
daiscog
85

Aunque Tomcat cancela el registro del controlador JDBC por usted, es una buena práctica limpiar todos los recursos creados por su aplicación web en la destrucción del contexto en caso de que se mude a otro contenedor de servlet que no realiza las comprobaciones de prevención de pérdida de memoria que hace Tomcat.

Sin embargo, la metodología de desregistro del controlador general es peligrosa. Algunos controladores devueltos por el DriverManager.getDrivers()método pueden haber sido cargados por el ClassLoader principal (es decir, el cargador de clases del contenedor de servlet), no el ClassLoader del contexto de la aplicación web (por ejemplo, pueden estar en la carpeta lib del contenedor, no en la aplicación web, y por lo tanto compartidos en todo el contenedor ) Anular el registro de estos afectará a cualquier otra aplicación web que pueda estar usándolos (o incluso al contenedor en sí).

Por lo tanto, uno debe verificar que el ClassLoader para cada controlador es el ClassLoader de la aplicación web antes de cancelar su registro. Entonces, en el método contextDestroyed () de ContextListener:

public final void contextDestroyed(ServletContextEvent sce) {
    // ... First close any background tasks which may be using the DB ...
    // ... Then close any DB connection pools ...

    // Now deregister JDBC drivers in this context's ClassLoader:
    // Get the webapp's ClassLoader
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    // Loop through all drivers
    Enumeration<Driver> drivers = DriverManager.getDrivers();
    while (drivers.hasMoreElements()) {
        Driver driver = drivers.nextElement();
        if (driver.getClass().getClassLoader() == cl) {
            // This driver was registered by the webapp's ClassLoader, so deregister it:
            try {
                log.info("Deregistering JDBC driver {}", driver);
                DriverManager.deregisterDriver(driver);
            } catch (SQLException ex) {
                log.error("Error deregistering JDBC driver {}", driver, ex);
            }
        } else {
            // driver was not registered by the webapp's ClassLoader and may be in use elsewhere
            log.trace("Not deregistering JDBC driver {} as it does not belong to this webapp's ClassLoader", driver);
        }
    }
}
daiscog
fuente
No debe ser if (cl.equals(driver.getClass().getClassLoader())) {?
user11153
66
@ user11153 No, estamos comprobando si es precisamente la misma instancia de ClassLoader, no si son dos instancias separadas que tienen el mismo valor.
Daiscog
44
Aunque los otros parecen manejar correctamente el problema en cuestión, causan problemas cuando el archivo de guerra se elimina y luego se reemplaza. En ese caso, los controladores se anulan el registro y nunca regresan; solo un reinicio de Tomcat puede sacarlo de ese agujero. Esta solución evita ese infierno.
OldCurmudgeon
Aquí casi siempre pero con código adicional de
gavenkoa
2
Si usa H2 o PostgreSQL, esto hace que el controlador no se registre nuevamente después de la recarga. Ambos controladores mantienen un estado de registro interno que no se borra si el controlador acaba de darse de baja del DriverManager. Dejé un comentario más detallado en github.com/spring-projects/spring-boot/issues/…
Marcel Stör
26

Veo que este problema surge mucho. Sí, Tomcat 7 lo anula automáticamente, pero ¿REALMENTE toma el control de su código y es una buena práctica de codificación? Seguramente USTED desea saber que tiene todo el código correcto para cerrar todos sus objetos, cerrar los subprocesos del grupo de conexiones de la base de datos y deshacerse de todas las advertencias. Ciertamente lo hago.

Así es como lo hago.

Paso 1: Registre un oyente

web.xml

<listener>
    <listener-class>com.mysite.MySpecialListener</listener-class>
</listener>

Paso 2: Implemente el oyente

com.mysite.MySpecialListener.java

public class MySpecialListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
        // On Application Startup, please…

        // Usually I'll make a singleton in here, set up my pool, etc.
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        // On Application Shutdown, please…

        // 1. Go fetch that DataSource
        Context initContext = new InitialContext();
        Context envContext  = (Context)initContext.lookup("java:/comp/env");
        DataSource datasource = (DataSource)envContext.lookup("jdbc/database");

        // 2. Deregister Driver
        try {
            java.sql.Driver mySqlDriver = DriverManager.getDriver("jdbc:mysql://localhost:3306/");
            DriverManager.deregisterDriver(mySqlDriver);
        } catch (SQLException ex) {
            logger.info("Could not deregister driver:".concat(ex.getMessage()));
        } 

        // 3. For added safety, remove the reference to dataSource for GC to enjoy.
        dataSource = null;
    }

}

Por favor, siéntase libre de comentar y / o agregar ...

sparkyspider
fuente
44
Pero DataSourceNO tiene un closemétodo
Jim
99
¿debería implementarse javax.servlet.ServletContextListener, no extender ApplicationContextListener?
Egaga
2
¿Hay alguna razón para el orden de esas operaciones en el método contextDestroyed? ¿Por qué haces Paso 1. Antes de realizar el paso 2., donde initContext, envContexty datasourceno se hace referencia a todos? Lo pregunto porque no entiendo el paso 3.
matthaeus
44
@matthaeus Creo que el Paso 1. no es necesario, lookupparece que solo obtiene algunos objetos que no necesita. El paso 3. es completamente inútil. Ciertamente no agrega ninguna seguridad y parece algo que los principiantes harían si no entienden cómo funciona GC. Simplemente iría con stackoverflow.com/a/5315467/897024 y eliminaría todos los controladores.
kapex
44
@kapep Eliminar todos los controladores es peligroso ya que algunos podrían compartirse en el contenedor. Vea mi respuesta para un enfoque que solo elimina los controladores cargados por el ClassLoader de su aplicación web.
daiscog
14

Esto es puramente un problema de registro / desregistro del controlador en el controlador de mysql o tombats webapp-classloader. Copie el controlador mysql en la carpeta lib de tomcats (para que se cargue directamente por jvm, no por tomcat), y el mensaje desaparecerá. Eso hace que el controlador mysql jdbc se descargue solo en el apagado de JVM, y a nadie le importan las pérdidas de memoria en ese momento.

viejo bastardo enfermo
fuente
2
eso no funciona ... Traté de copiar el controlador jdbc si dice: TOMCAT_HOME / lib / postgresql-9.0-801.jdbc4.jar - Tomcat 7.0 \ lib \ postgresql-9.0-801.jdbc4.jar sin resultados ...
FAjir
55
@Florito: también debe eliminarlo de sus aplicaciones web WEB-INF / lib
Collin Peters el
8

Si recibe este mensaje de una guerra construida por Maven, cambie el alcance del controlador JDBC que se proporciona y coloque una copia en el directorio lib. Me gusta esto:

<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>5.1.18</version>
  <!-- put a copy in /usr/share/tomcat7/lib -->
  <scope>provided</scope>
</dependency>
TimP
fuente
8

Solución para implementaciones por aplicación

Este es un oyente que escribí para resolver el problema: detecta automáticamente si el controlador se ha registrado y actúa en consecuencia.

Importante: está destinado a usarse SOLO cuando el controlador jar se implementa en WEB-INF / lib , no en Tomcat / lib, como muchos sugieren, para que cada aplicación pueda cuidar su propio controlador y ejecutarse en un Tomcat intacto . Esa es la forma en que debería ser en mi humilde opinión.

Simplemente configure el oyente en su web.xml antes que cualquier otro y disfrute.

agregue cerca de la parte superior de web.xml :

<listener>
    <listener-class>utils.db.OjdbcDriverRegistrationListener</listener-class>    
</listener>

guardar como utils / db / OjdbcDriverRegistrationListener.java :

package utils.db;

import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Enumeration;

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

import oracle.jdbc.OracleDriver;

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

/**
 * Registers and unregisters the Oracle JDBC driver.
 * 
 * Use only when the ojdbc jar is deployed inside the webapp (not as an
 * appserver lib)
 */
public class OjdbcDriverRegistrationListener implements ServletContextListener {

    private static final Logger LOG = LoggerFactory
            .getLogger(OjdbcDriverRegistrationListener.class);

    private Driver driver = null;

    /**
     * Registers the Oracle JDBC driver
     */
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        this.driver = new OracleDriver(); // load and instantiate the class
        boolean skipRegistration = false;
        Enumeration<Driver> drivers = DriverManager.getDrivers();
        while (drivers.hasMoreElements()) {
            Driver driver = drivers.nextElement();
            if (driver instanceof OracleDriver) {
                OracleDriver alreadyRegistered = (OracleDriver) driver;
                if (alreadyRegistered.getClass() == this.driver.getClass()) {
                    // same class in the VM already registered itself
                    skipRegistration = true;
                    this.driver = alreadyRegistered;
                    break;
                }
            }
        }

        try {
            if (!skipRegistration) {
                DriverManager.registerDriver(driver);
            } else {
                LOG.debug("driver was registered automatically");
            }
            LOG.info(String.format("registered jdbc driver: %s v%d.%d", driver,
                    driver.getMajorVersion(), driver.getMinorVersion()));
        } catch (SQLException e) {
            LOG.error(
                    "Error registering oracle driver: " + 
                            "database connectivity might be unavailable!",
                    e);
            throw new RuntimeException(e);
        }
    }

    /**
     * Deregisters JDBC driver
     * 
     * Prevents Tomcat 7 from complaining about memory leaks.
     */
    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
        if (this.driver != null) {
            try {
                DriverManager.deregisterDriver(driver);
                LOG.info(String.format("deregistering jdbc driver: %s", driver));
            } catch (SQLException e) {
                LOG.warn(
                        String.format("Error deregistering driver %s", driver),
                        e);
            }
            this.driver = null;
        } else {
            LOG.warn("No driver to deregister");
        }

    }

}
Andrea Ratto
fuente
Consejo: A partir de Servlet 3.0, puede anotar su clase @WebListenery omitir la web.xmlconfiguración.
Basil Bourque
Es cierto, solo asegúrese de que esté seleccionado con una prioridad lo suficientemente alta, para que nadie necesite usar el controlador antes o después.
Andrea Ratto
6

Agregaré a esto algo que encontré en los foros de Spring. Si mueve el jar del controlador JDBC a la carpeta lib de tomcat, en lugar de implementarlo con su aplicación web, la advertencia parece desaparecer. Puedo confirmar que esto funcionó para mí

http://forum.springsource.org/showthread.php?87335-Failure-to-unregister-the-MySQL-JDBC-Driver&p=334883#post334883

Collin Peters
fuente
1
Creo que esto ofrece una mejor solución: simplemente subclasifique su DatasourceManager y anule el método de cierre para agregar la anulación del registro. Entonces Spring lo manejará cuando destruya su contexto, y Tomcat no le dará el registro SEVERO, y no tiene que mover su controlador JDBC al directorio lib. Aquí está el mensaje original del antiguo foro de primavera
Adam
6

Descubrí que implementar un método simple destroy () para cancelar el registro de cualquier controlador JDBC funciona bien.

/**
 * Destroys the servlet cleanly by unloading JDBC drivers.
 * 
 * @see javax.servlet.GenericServlet#destroy()
 */
public void destroy() {
    String prefix = getClass().getSimpleName() +" destroy() ";
    ServletContext ctx = getServletContext();
    try {
        Enumeration<Driver> drivers = DriverManager.getDrivers();
        while(drivers.hasMoreElements()) {
            DriverManager.deregisterDriver(drivers.nextElement());
        }
    } catch(Exception e) {
        ctx.log(prefix + "Exception caught while deregistering JDBC drivers", e);
    }
    ctx.log(prefix + "complete");
}
Darrin M. Gorski
fuente
55
Esto es potencialmente inseguro en un entorno compartido, ya que es posible que no desee cancelar el registro de todos los controladores JDBC que están disponibles. Vea mi respuesta para un enfoque más seguro. Además, esto realmente debe hacerse en un ServletContextListener, no en una base por servlet, ya que su controlador JDBC se comparte entre todos sus servlets en su aplicación web.
Daiscog
3

Para evitar esta pérdida de memoria, simplemente anule el registro del controlador al apagar el contexto.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.mywebsite</groupId>
    <artifactId>emusicstore</artifactId>
    <version>1.0-SNAPSHOT</version>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.7.0</version>
                <configuration>
                    <source>1.9</source>
                    <target>1.9</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <!-- ... -->

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>4.0.1.Final</version>
        </dependency>

        <dependency>
            <groupId>org.hibernate.javax.persistence</groupId>
            <artifactId>hibernate-jpa-2.0-api</artifactId>
            <version>1.0.1.Final</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.11</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/javax.servlet/servlet-api -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

</project>

MyWebAppContextListener.java

package com.emusicstore.utils;

import com.mysql.cj.jdbc.AbandonedConnectionCleanupThread;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Enumeration;

public class MyWebAppContextListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        System.out.println("************** Starting up! **************");
    }

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
        System.out.println("************** Shutting down! **************");
        System.out.println("Destroying Context...");
        System.out.println("Calling MySQL AbandonedConnectionCleanupThread checkedShutdown");
        AbandonedConnectionCleanupThread.checkedShutdown();

        ClassLoader cl = Thread.currentThread().getContextClassLoader();

        Enumeration<Driver> drivers = DriverManager.getDrivers();
        while (drivers.hasMoreElements()) {
            Driver driver = drivers.nextElement();

            if (driver.getClass().getClassLoader() == cl) {
                try {
                    System.out.println("Deregistering JDBC driver {}");
                    DriverManager.deregisterDriver(driver);

                } catch (SQLException ex) {
                    System.out.println("Error deregistering JDBC driver {}");
                    ex.printStackTrace();
                }
            } else {
                System.out.println("Not deregistering JDBC driver {} as it does not belong to this webapp's ClassLoader");
            }
        }
    }

}

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                             http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <listener>
        <listener-class>com.emusicstore.utils.MyWebAppContextListener</listener-class>
    </listener>

<!-- ... -->

</web-app>

Fuente que me inspiró para esta corrección de errores.

Rey Mago
fuente
2

Estaba teniendo un problema similar, pero además recibía un error de Java Heap Space cada vez que modificaba / guardaba páginas JSP con el servidor Tomcat ejecutándose, por lo tanto, el contexto no se recargó por completo.

Mis versiones fueron Apache Tomcat 6.0.29 y JDK 6u12.

La actualización de JDK a 6u21 como se sugiere en la sección Referencias de la URL http://wiki.apache.org/tomcat/MemoryLeakProtection resolvió el problema de Java Heap Space (el contexto ahora se recarga correctamente ) aunque el error del controlador JDBC todavía aparece.

Francisco Alvarado
fuente
0

Encontré el mismo problema con Tomcat versión 6.026.

Utilicé Mysql JDBC.jar en la biblioteca de WebAPP, así como en TOMCAT Lib.

Para arreglar lo anterior quitando el Jar de la carpeta lib de TOMCAT.

Entonces, lo que entiendo es que TOMCAT está manejando la pérdida de memoria JDBC correctamente. Pero si el jar MYSQL Jdbc está duplicado en WebApp y Tomcat Lib, Tomcat solo podrá manejar el jar presente en la carpeta Tomcat Lib.

Bharat
fuente
0

Me enfrenté a este problema cuando estaba implementando mi aplicación Grails en AWS. Esto es cuestión del controlador predeterminado JDBC org.h2 driver. Como puede ver esto en Datasource.groovy dentro de su carpeta de configuración. Como puede ver abajo :

dataSource {
    pooled = true
    jmxExport = true
    driverClassName = "org.h2.Driver"   // make this one comment
    username = "sa"
    password = ""
}

Comente esas líneas siempre que se mencione org.h2.Driver en el archivo datasource.groovy, si no está utilizando esa base de datos. De lo contrario, debe descargar el archivo jar de la base de datos.

Gracias .

PyDevSRS
fuente
0

Este error me ocurrió en una aplicación Grails con el controlador JTDS 1.3.0 (SQL Server). El problema fue un inicio de sesión incorrecto en SQL Server. Después de resolver este problema (en SQL Server), mi aplicación se implementó correctamente en Tomcat. Consejo: vi el error en stacktrace.log

Cantoni
fuente