Pour éviter une fuite de mémoire, le pilote JDBC a été annulé de force

325

Je reçois ce message lorsque j'exécute mon application Web. Cela fonctionne bien mais je reçois ce message lors de l'arrêt.

GRAVE: Une application Web a enregistré le pilote JBDC [oracle.jdbc.driver.OracleDriver] mais n'a pas réussi à le désinscrire lorsque l'application Web a été arrêtée. Pour éviter une fuite de mémoire, le pilote JDBC a été annulé de force.

Toute aide appréciée.

mona
la source
4
Pourrait être un doublon de stackoverflow.com/questions/2604630/…
skaffman

Réponses:

301

Depuis la version 6.0.24, Tomcat est livré avec une fonction de détection de fuite de mémoire , qui à son tour peut conduire à ce type de messages d'avertissement lorsqu'il y a un pilote compatible JDBC 4.0 dans la webapp /WEB-INF/libqui s'enregistre automatiquement au démarrage de la webapp à l'aide de l' ServiceLoaderAPI , mais qui ne s'est pas désenregistré automatiquement lors de l'arrêt de la webapp. Ce message est purement informel, Tomcat a déjà pris les mesures de prévention des fuites de mémoire en conséquence.

Que pouvez-vous faire?

  1. Ignorez ces avertissements. Tomcat fait bien son travail. Le bogue réel est dans le code de quelqu'un d'autre (le pilote JDBC en question), pas dans le vôtre. Soyez heureux que Tomcat ait fait son travail correctement et attendez que le fournisseur de pilotes JDBC le corrige pour que vous puissiez mettre à niveau le pilote. D'un autre côté, vous n'êtes pas censé supprimer un pilote JDBC dans les applications Web /WEB-INF/lib, mais uniquement dans les serveurs /lib. Si vous le conservez toujours dans l'application Web /WEB-INF/lib, vous devez l'enregistrer et le désenregistrer manuellement à l'aide de a ServletContextListener.

  2. Rétrogradez vers Tomcat 6.0.23 ou une version antérieure afin de ne pas être dérangé par ces avertissements. Mais il gardera silencieusement la fuite de mémoire. Je ne sais pas si c'est bon à savoir après tout. Ce type de fuites de mémoire est l'une des principales causes des OutOfMemoryErrorproblèmes lors des déploiements à chaud Tomcat.

  3. Déplacez le pilote JDBC vers le /libdossier Tomcat et disposez d'une source de données regroupée de connexions pour gérer le pilote. Notez que le DBCP intégré de Tomcat ne désenregistre pas correctement les pilotes à la fermeture. Voir aussi le bogue DBCP-322 qui est fermé en tant que WONTFIX. Vous préférez remplacer DBCP par un autre pool de connexions qui fait mieux son travail que DBCP. Par exemple, HikariCP , BoneCP ou peut-être le pool JDBC Tomcat .

BalusC
la source
25
C'est un bon conseil. Ce n'est pas un avertissement de fuite de mémoire, c'est un avertissement que Tomcat a pris des mesures forcées pour empêcher une fuite
mat b
49
Si l'option (1) est le chemin à parcourir, pourquoi Tomcat les enregistre-t-il comme SÉVÈRE, cependant? SÉVÈRE signifie pour moi «envoyer une page à l'administrateur» et non «ignorer».
Peter Becker
7
Pourquoi ne pas le faire vous-même plutôt que de vous attendre à ce que Tomcat le fasse. À mon avis, ce n'est pas le travail de Tomcat de nettoyer notre code en désordre. Voir ma réponse ci-dessous.
sparkyspider
2
@sproketboy: Hein? Attribuiez-vous des artefacts JDBC en tant que champs d'une classe qui est à son tour stockée dans la session HTTP?
BalusC
2
Je suppose que c'est généralement la raison 3 (avoir la bibliothèque dans le wAR par opposition à lib).
lapo
160

Dans la méthode contextDestroyed () de votre écouteur de contexte de servlet, annulez manuellement l'enregistrement des pilotes:

// 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
la source
3
Ça marche! javabeat.net/servletcontextlistener-example peut aider à implémenter l'écouteur de contexte de servlet
Vadim Zin4uk
17
Ceci est potentiellement dangereux dans un environnement partagé car vous ne souhaiterez peut-être pas annuler l'enregistrement de tous les pilotes JDBC disponibles. Voir ma réponse pour une approche plus sûre.
daiscog
85

Bien que Tomcat désenregistre de force le pilote JDBC pour vous, il est néanmoins de bonne pratique de nettoyer toutes les ressources créées par votre application Web lors de la destruction du contexte au cas où vous passez à un autre conteneur de servlet qui ne fait pas les vérifications de prévention des fuites de mémoire que fait Tomcat.

Cependant, la méthodologie de désinscription globale des pilotes est dangereuse. Certains pilotes renvoyés par la DriverManager.getDrivers()méthode peuvent avoir été chargés par le ClassLoader parent (c'est-à-dire le chargeur de classe du conteneur de servlet) et non par le ClassLoader du contexte de l'application Web (par exemple, ils peuvent se trouver dans le dossier lib du conteneur, pas celui de l'application Web, et donc être partagés sur l'ensemble du conteneur. ). Le retrait de ceux-ci affectera toutes les autres applications Web qui pourraient les utiliser (ou même le conteneur lui-même).

Par conséquent, il convient de vérifier que le ClassLoader de chaque pilote est le ClassLoader de la webapp avant de le désenregistrer. Ainsi, dans la méthode contextDestroyed () de votre 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
la source
Ne devrait pas l'être if (cl.equals(driver.getClass().getClassLoader())) {?
user11153
6
@ user11153 Non, nous vérifions s'il s'agit précisément de la même instance ClassLoader, pas s'il s'agit de deux instances distinctes de valeur égale.
daiscog
4
Bien que les autres semblent gérer correctement le problème en question, ils provoquent des problèmes lorsque le fichier war est supprimé puis remplacé. Dans ce cas, les pilotes sont désinscrits et ne reviennent jamais - seul un redémarrage de Tomcat peut vous sortir de ce trou. Cette solution évite cet enfer.
OldCurmudgeon
Ici, la plupart du temps, mais avec un code de gestion MySQL / MariaDB supplémentaire github.com/spring-projects/spring-boot/issues/2612
gavenkoa
2
Si vous utilisez H2 ou PostgreSQL, le pilote ne sera pas enregistré à nouveau lors du rechargement. Les deux pilotes conservent un état d'enregistrement interne qui n'est pas effacé si le pilote est simplement désinscrit du DriverManager. J'ai laissé un commentaire plus détaillé sur github.com/spring-projects/spring-boot/issues/…
Marcel Stör
26

Je vois que ce problème revient souvent. Oui, Tomcat 7 le désenregistre automatiquement, mais est-ce que vous prenez VRAIMENT le contrôle de votre code et une bonne pratique de codage? Vous voulez sûrement savoir que vous avez tout le code correct en place pour fermer tous vos objets, fermer les threads du pool de connexion à la base de données et vous débarrasser de tous les avertissements. Je fais certainement.

Voilà comment je le fais.

Étape 1: enregistrer un auditeur

web.xml

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

Étape 2: implémenter l'écouteur

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

}

N'hésitez pas à commenter et / ou ajouter ...

sparkyspider
la source
4
Mais DataSourcen'a PAS de closeméthode
Jim
9
Doit-il implémenter javax.servlet.ServletContextListener, pas étendre ApplicationContextListener?
egaga
2
Y a-t-il une raison pour l'ordre de ces opérations dans la méthode contextDestroyed? Pourquoi faites - vous l' étape 1. avant de faire l' étape 2., où initContext, envContextet datasourcene sont pas référencés du tout? Je demande parce que je ne comprends pas l'étape 3.
matthaeus
4
@matthaeus Je pense que l'étape 1. n'est pas nécessaire, lookupsemble simplement obtenir des objets dont vous n'avez pas besoin. L'étape 3. est complètement inutile. Cela n'ajoute certainement aucune sécurité et ressemble à quelque chose que les débutants feraient qui ne comprennent pas comment fonctionne GC. Je voudrais simplement aller avec stackoverflow.com/a/5315467/897024 et supprimer tous les pilotes.
kapex
4
@kapep La suppression de tous les pilotes est dangereuse car certains peuvent être partagés sur le conteneur. Voir ma réponse pour une approche qui supprime uniquement les pilotes chargés par le ClassLoader de votre application Web.
daiscog
14

Il s'agit uniquement d'un problème d'enregistrement / de désinscription du pilote dans le pilote mysql ou le chargeur de classe webapp tomcats. Copiez le pilote mysql dans le dossier lib tomcats (afin qu'il soit chargé directement par jvm, pas par tomcat), et le message disparaîtra. Cela fait que le pilote mysql jdbc ne doit être déchargé qu'à l'arrêt de la JVM, et personne ne se soucie alors des fuites de mémoire.

vieux bâtard malade
la source
2
cela ne fonctionne pas ... J'ai essayé de copier le pilote jdbc vous a dit: TOMCAT_HOME / lib / postgresql-9.0-801.jdbc4.jar - Tomcat 7.0 \ lib \ postgresql-9.0-801.jdbc4.jar sans résultat ...
FAjir
5
@Florito - vous devez également le supprimer de vos applications Web WEB-INF / lib
Collin Peters
8

Si vous recevez ce message d'une guerre construite par Maven, changez la portée du pilote JDBC en fourni et mettez-en une copie dans le répertoire lib. Comme ça:

<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
la source
8

Solution pour les déploiements par application

Voici un écouteur que j'ai écrit pour résoudre le problème: il détecte automatiquement si le pilote s'est enregistré et agit en conséquence.

Important: il est destiné à être utilisé UNIQUEMENT lorsque le fichier pilote est déployé dans WEB-INF / lib , pas dans Tomcat / lib, comme beaucoup le suggèrent, afin que chaque application puisse prendre soin de son propre pilote et s'exécuter sur un Tomcat intact. . C'est ainsi que cela devrait être à mon humble avis.

Configurez simplement l'écouteur dans votre web.xml avant tout autre et profitez-en.

ajoutez en haut de web.xml :

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

enregistrer sous 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
la source
Astuce: à partir de Servlet 3.0, vous pouvez annoter votre classe avec @WebListeneret omettre la web.xmlconfiguration.
Basil Bourque
Certes, assurez-vous simplement qu'il est capté avec une priorité suffisamment élevée, afin que personne n'ait besoin d'utiliser le pilote avant ou après.
Andrea Ratto
6

J'ajouterai à ce quelque chose que j'ai trouvé sur les forums de printemps. Si vous déplacez votre jar de pilote JDBC vers le dossier lib tomcat, au lieu de le déployer avec votre webapp, l'avertissement semble disparaître. Je peux confirmer que cela a fonctionné pour moi

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

Collin Peters
la source
1
Je pense que cela offre une meilleure solution - il suffit de sous-classer votre DatasourceManager et de remplacer la méthode close pour ajouter la désinscription. Ensuite, Spring le gérera quand il détruira son contexte, et Tomcat ne vous donnera pas le journal SEVERE, et vous n'avez pas à déplacer votre pilote JDBC dans le répertoire lib. Voici le message d'origine de l'ancien forum de printemps
Adam
6

J'ai trouvé que l'implémentation d'une méthode destroy () simple pour désenregistrer les pilotes JDBC fonctionne 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
la source
5
Ceci est potentiellement dangereux dans un environnement partagé car vous ne souhaiterez peut-être pas annuler l'enregistrement de tous les pilotes JDBC disponibles. Voir ma réponse pour une approche plus sûre. En outre, cela devrait vraiment être fait dans un ServletContextListener, pas sur une base par servlet car votre pilote JDBC est partagé entre tous vos servlets dans votre webapp.
daiscog
3

Pour éviter cette fuite de mémoire, désenregistrez simplement le pilote à l'arrêt du contexte.

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>

Source qui m'a inspiré pour cette correction de bogue.

Roi-sorcier
la source
2

J'avais un problème similaire, mais en plus, j'obtenais une erreur Java Heap Space chaque fois que je modifiais / sauvegardais des pages JSP avec le serveur Tomcat en cours d'exécution, donc le contexte n'était pas complètement rechargé.

Mes versions étaient Apache Tomcat 6.0.29 et JDK 6u12.

La mise à niveau de JDK vers 6u21 comme suggéré dans la section Références de l'URL http://wiki.apache.org/tomcat/MemoryLeakProtection a résolu le problème de l'espace de tas Java (le contexte se recharge maintenant OK) bien que l'erreur du pilote JDBC s'affiche toujours.

Francisco Alvarado
la source
0

J'ai trouvé le même problème avec Tomcat version 6.026.

J'ai utilisé le Mysql JDBC.jar dans la bibliothèque WebAPP ainsi que dans TOMCAT Lib.

Pour corriger ce qui précède en supprimant le Jar du dossier lib TOMCAT.

Donc, ce que je comprends, c'est que TOMCAT gère correctement la fuite de mémoire JDBC. Mais si le jar MYSQL Jdbc est dupliqué dans WebApp et Tomcat Lib, Tomcat ne pourra gérer que le jar présent dans le dossier Tomcat Lib.

Bharat
la source
0

J'ai rencontré ce problème lorsque je déployais mon application Grails sur AWS. Il s'agit du pilote par défaut JDBC org.h2 driver. Comme vous pouvez le voir dans le Datasource.groovy à l'intérieur de votre dossier de configuration. Comme vous pouvez le voir ci-dessous:

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

Commentez ces lignes partout où il est mentionné org.h2.Driver dans le fichier , si vous n'utilisez pas cette base de données. Sinon, vous devez télécharger ce fichier jar de base de données.

Merci .

PyDevSRS
la source
0

Cette erreur m'est arrivée dans une application Grails avec le pilote JTDS 1.3.0 (SQL Server). Le problème était une connexion incorrecte dans SQL Server. Après avoir résolu ce problème (dans SQL Server), mon application a été correctement déployée dans Tomcat. Astuce: j'ai vu l'erreur dans stacktrace.log

cantoni
la source