Per evitare una perdita di memoria, il driver JDBC è stato forzatamente annullato la registrazione


325

Ricevo questo messaggio quando eseguo la mia applicazione web. Funziona bene ma ricevo questo messaggio durante l'arresto.

SEVERE: un'applicazione Web ha registrato il driver JBDC [oracle.jdbc.driver.OracleDriver] ma non è stato possibile annullare la registrazione quando l'applicazione web è stata arrestata. Per evitare una perdita di memoria, il driver JDBC è stato forzatamente annullato la registrazione.

Qualsiasi aiuto apprezzato.


4
Potrebbe essere duplicato di stackoverflow.com/questions/2604630/...
skaffman

Risposte:


301

Dalla versione 6.0.24, Tomcat viene fornito con una funzione di rilevamento delle perdite di memoria , che a sua volta può portare a questo tipo di messaggi di avviso quando c'è un driver compatibile con JDBC 4.0 nelle webapp /WEB-INF/libche si registra automaticamente all'avvio di webapp usando l' ServiceLoaderAPI , ma che non si è auto- registrato durante l'arresto di webapp. Questo messaggio è puramente informale, Tomcat ha già adottato di conseguenza l'azione di prevenzione della perdita di memoria.

Cosa sai fare?

  1. Ignora quegli avvisi. Tomcat sta facendo bene il suo lavoro. Il vero bug è nel codice di qualcun altro (il driver JDBC in questione), non nel tuo. Sii felice che Tomcat abbia svolto correttamente il suo lavoro e attendi fino a quando il fornitore del driver JDBC non lo risolverà in modo da poter aggiornare il driver. D'altra parte, non dovresti rilasciare un driver JDBC in webapp /WEB-INF/lib, ma solo in server /lib. Se lo conservi ancora in webapp /WEB-INF/lib, devi registrarti manualmente e annullare la registrazione utilizzando a ServletContextListener.

  2. Esegui il downgrade a Tomcat 6.0.23 o precedente in modo da non essere disturbato da tali avvisi. Ma continuerà silenziosamente a perdere memoria. Non sono sicuro che sia bello sapere dopo tutto. Questo tipo di perdite di memoria è una delle principali cause alla base dei OutOfMemoryErrorproblemi durante le hotdeployments di Tomcat.

  3. Spostare il driver JDBC nella /libcartella Tomcat e disporre di un'origine dati in pool di connessioni per gestire il driver. Si noti che il DBCP integrato di Tomcat non annulla la registrazione corretta dei driver alla chiusura. Vedi anche il bug DBCP-322 che è chiuso come WONTFIX. Preferisci sostituire DBCP con un altro pool di connessioni che sta facendo il suo lavoro meglio di DBCP. Ad esempio HikariCP , BoneCP o forse Tomcat JDBC Pool .


25
Questo è un buon consiglio Non è un avvertimento di una perdita di memoria, è un avvertimento che Tomcat ha intrapreso alcune azioni forzate per prevenire una perdita
matt b

49
Se l'opzione (1) è la strada da percorrere, perché Tomcat li registra come SEVERE? SEMPRE per me significa "pagina l'amministratore", non "ignora".
Peter Becker,

7
Perché non farlo da soli - piuttosto che aspettarti che Tomcat lo faccia. Secondo me, non è compito di Tomcat ripulire il nostro codice disordinato. Vedi la mia risposta qui sotto.
sparkyspider,

2
@sproketboy: Huh? Stavi assegnando artefatti JDBC come campi di una classe che è a sua volta archiviata nella sessione HTTP?
BalusC,

2
Immagino che di solito sia la ragione 3 (avere la biblioteca nella guerra invece di lib).
lapo,

160

Nel tuo listener di contesto servlet contextDestroyed () metodo, annulla la registrazione manuale dei driver:

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

3
Funziona! javabeat.net/servletcontextlistener-example può aiutare a implementare il listener di contesto servlet
Vadim Zin4uk,

17
Questo non è potenzialmente sicuro in un ambiente condiviso poiché potresti non voler annullare la registrazione di tutti i driver JDBC disponibili. Vedi la mia risposta per un approccio più sicuro.
daiscog,

85

Sebbene Tomcat annulli forzatamente la registrazione del driver JDBC per te, è comunque buona norma ripulire tutte le risorse create dal tuo webapp in caso di distruzione del contesto nel caso in cui ti sposti in un altro contenitore servlet che non esegue i controlli di prevenzione delle perdite di memoria che Tomcat esegue.

Tuttavia, la metodologia di annullamento della registrazione del conducente è pericolosa. Alcuni driver restituiti dal DriverManager.getDrivers()metodo potrebbero essere stati caricati dal ClassLoader padre (ovvero, il classloader del contenitore servlet) non dal ClassLoader del contesto webapp (ad esempio, potrebbero essere nella cartella lib del contenitore, non dal webapp, e quindi condivisi nell'intero contenitore ). L'annullamento della registrazione influirà su qualsiasi altra webapp che potrebbe utilizzarli (o persino sul contenitore stesso).

Pertanto, si dovrebbe verificare che ClassLoader per ciascun driver sia ClassLoader del webapp prima di annullare la registrazione. Quindi, nel metodo contextDestroyed () del tuo 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);
        }
    }
}

Non dovrebbe essere if (cl.equals(driver.getClass().getClassLoader())) {?
user11153,

6
@ user11153 No, stiamo verificando se si tratta esattamente della stessa istanza ClassLoader, non se si tratta di due istanze separate che hanno lo stesso valore.
daiscog,

4
Sebbene gli altri sembrano gestire correttamente il problema in questione, causano problemi quando il file di guerra viene eliminato e quindi sostituito. In tal caso i driver vengono annullati e non tornano più: solo un riavvio Tomcat può farti uscire da quel buco. Questa soluzione evita quell'inferno.
OldCurmudgeon,

Qui per lo più lo stesso, ma con il codice di gestione MySQL / MariaDB aggiuntivo github.com/spring-projects/spring-boot/issues/2612
gavenkoa,

2
Se si utilizza H2 o PostgreSQL, il driver non verrà più registrato al ricaricamento. Entrambi i driver mantengono uno stato di registrazione interno che non viene cancellato se il driver viene appena cancellato da DriverManager. Ho lasciato un commento più dettagliato su github.com/spring-projects/spring-boot/issues/…
Marcel Stör

26

Vedo questo problema emergere molto. Sì, Tomcat 7 lo annulla automaticamente, ma ciò prende VERAMENTE il controllo del codice e una buona pratica di codifica? Sicuramente VUOI sapere che hai tutto il codice corretto per chiudere tutti i tuoi oggetti, chiudere i thread del pool di connessioni al database ed eliminare tutti gli avvisi. Certamente.

Questo è come lo faccio.

Passaggio 1: registra un ascoltatore

web.xml

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

Passaggio 2: implementare il listener

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

}

Non esitate a commentare e / o aggiungere ...


4
Ma DataSourceNON ha un closemetodo
Jim

9
dovrebbe essere implementa javax.servlet.ServletContextListener, non estende ApplicationContextListener?
egaga,

2
C'è un motivo per l'ordine di quelle operazioni nel metodo contextDestroyed? Perché lo fai passaggio 1. prima di fare il punto 2., in cui initContext, envContexte datasourcenon si fa riferimento a tutti? Lo sto chiedendo perché non capisco il passaggio 3.
matthaeus

4
@matthaeus Penso che il passaggio 1. non sia necessario, lookupsembra solo ottenere alcuni oggetti che non ti servono. Il passaggio 3. è completamente inutile. Certamente non aggiunge alcuna sicurezza e sembra che farebbero i principianti che non capiscono come funziona GC. Vorrei solo andare con stackoverflow.com/a/5315467/897024 e rimuovere tutti i driver.
kapex,

4
@kapep La rimozione di tutti i driver è pericolosa poiché alcuni potrebbero essere condivisi nel contenitore. Vedi la mia risposta per un approccio che rimuove solo i driver caricati dal ClassLoader della tua webapp.
daiscog,

14

Questo è puramente un problema di registrazione / cancellazione del driver nel driver mysql`s o tomcats webapp-classloader. Copia il driver mysql nella cartella lib di tomcats (quindi viene caricato direttamente da jvm, non da tomcat) e il messaggio sparirà. Questo fa sì che il driver mysql jdbc venga scaricato solo allo spegnimento di JVM, e nessuno si preoccupa delle perdite di memoria.


2
che non funziona ... Ho provato a copiare il driver jdbc hai detto: TOMCAT_HOME / lib / postgresql-9.0-801.jdbc4.jar - Tomcat 7.0 \ lib \ postgresql-9.0-801.jdbc4.jar senza risultati ...
FAjir,

5
@Florito - devi rimuoverlo anche dalle tue web-app WEB-INF / lib
Collin Peters,

8

Se stai ricevendo questo messaggio da una guerra costruita da Maven, modifica l'ambito del driver JDBC in fornito e mettine una copia nella directory lib. Come questo:

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

8

Soluzione per distribuzioni per app

Questo è un ascoltatore che ho scritto per risolvere il problema: rileva automaticamente se il conducente si è registrato e agisce di conseguenza

Importante: è pensato per essere utilizzato SOLO quando il jar del driver è distribuito in WEB-INF / lib , non in Tomcat / lib, come molti suggeriscono, in modo che ogni applicazione possa prendersi cura del proprio driver ed eseguire su un Tomcat intatto . Questo è il modo in cui dovrebbe essere IMHO.

Basta configurare il listener nel tuo web.xml prima di ogni altro e divertiti.

aggiungi nella parte superiore di web.xml :

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

salva come 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");
        }

    }

}

Suggerimento: a partire da Servlet 3.0, puoi annotare la tua classe con @WebListenere omettere la web.xmlconfigurazione.
Basil Bourque,

È vero, assicurati solo che sia raccolto con una priorità abbastanza alta, in modo che nessuno debba usare il driver prima o dopo.
Andrea Ratto,

6

Aggiungerò a questo qualcosa che ho trovato nei forum di primavera. Se sposti il ​​tuo jar del driver JDBC nella cartella lib di tomcat, invece di distribuirlo con la tua webapp, l'avviso sembra scomparire. Posso confermare che questo ha funzionato per me

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


1
Penso che ciò offra una soluzione migliore: basta sottoclassare il DatasourceManager e sovrascrivere il metodo di chiusura per aggiungere la cancellazione. Quindi Spring lo gestirà quando distruggerà il suo contesto, e Tomcat non ti darà il registro SEVERE e non dovrai spostare il tuo driver JDBC nella directory lib. Ecco l'originale messaggio dal vecchio forum di primavera
Adam,

6

Ho scoperto che l'implementazione di un semplice metodo destroy () per annullare la registrazione di qualsiasi driver JDBC funziona correttamente.

/**
 * 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");
}

5
Questo non è potenzialmente sicuro in un ambiente condiviso poiché potresti non voler annullare la registrazione di tutti i driver JDBC disponibili. Vedi la mia risposta per un approccio più sicuro. Inoltre, questo dovrebbe davvero essere fatto in un ServletContextListener, non su una base per servlet poiché il driver JDBC è condiviso su tutti i servlet nella tua webapp.
daiscog,

3

Per evitare questa perdita di memoria, è sufficiente annullare la registrazione del driver all'arresto del contesto.

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>

Fonte che mi ha ispirato per questa correzione di bug.


2

Stavo riscontrando un problema simile, ma in più ho riscontrato un errore Java Heap Space ogni volta che ho modificato / salvato le pagine JSP con il server Tomcat in esecuzione, quindi il contesto non è stato completamente ricaricato.

Le mie versioni erano Apache Tomcat 6.0.29 e JDK 6u12.

L'aggiornamento di JDK a 6u21 come suggerito nella sezione Riferimenti dell'URL http://wiki.apache.org/tomcat/MemoryLeakProtection ha risolto il problema di Java Heap Space (il contesto ora ricarica OK) sebbene l'errore del driver JDBC sia ancora visualizzato.


0

Ho riscontrato lo stesso problema con Tomcat versione 6.026.

Ho usato Mysql JDBC.jar nella libreria WebAPP e in TOMCAT Lib.

Per risolvere quanto sopra rimuovendo il Jar dalla cartella lib di TOMCAT.

Quindi quello che capisco è che TOMCAT sta gestendo correttamente la perdita di memoria JDBC. Ma se il jar MdQL Jdbc è duplicato in WebApp e Tomcat Lib, Tomcat sarà in grado di gestire solo il jar presente nella cartella Tomcat Lib.


0

Ho riscontrato questo problema durante la distribuzione della mia applicazione Grails su AWS. Si tratta del driver org.h2 predefinito del driver JDBC . Come puoi vedere in Datasource.groovy nella cartella di configurazione. Come puoi vedere di seguito:

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

Commenta quelle righe ovunque sia menzionato org.h2.Driver nel file datasource.groovy, se non stai utilizzando quel database. Altrimenti devi scaricare quel file jar del database.

Grazie .


0

Questo errore mi è successo in un'applicazione Grails con il driver JTDS 1.3.0 (SQL Server). Il problema era un accesso errato in SQL Server. Dopo aver risolto questo problema (in SQL Server) la mia app è stata correttamente distribuita in Tomcat. Suggerimento: ho visto l'errore in stacktrace.log

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.