Para evitar um vazamento de memória, o driver JDBC foi forçado a não ser registrado

325

Estou recebendo essa mensagem quando executo meu aplicativo da web. Funciona bem, mas recebo esta mensagem durante o desligamento.

GRAVE: Um aplicativo da web registrou o driver JBDC [oracle.jdbc.driver.OracleDriver], mas falhou em cancelar o registro quando o aplicativo da web foi parado. Para evitar um vazamento de memória, o driver JDBC não foi registrado à força.

Qualquer ajuda apreciada.

mona
fonte
4
Poderia ser duplicado de stackoverflow.com/questions/2604630/...
skaffman

Respostas:

301

Desde a versão 6.0.24, o Tomcat é fornecido com um recurso de detecção de vazamento de memória , que por sua vez pode levar a esse tipo de mensagem de aviso quando há um driver compatível com JDBC 4.0 nos aplicativos da web /WEB-INF/libque se registra automaticamente durante a inicialização do aplicativo usando a ServiceLoaderAPI , mas que não se registrou automaticamente durante o desligamento do webapp. Esta mensagem é puramente informal, o Tomcat já executou a ação de prevenção de vazamento de memória em conformidade.

O que você pode fazer?

  1. Ignore esses avisos. O Tomcat está fazendo seu trabalho corretamente. O bug real está no código de outra pessoa (o driver JDBC em questão), não no seu. Fique feliz que o Tomcat tenha feito seu trabalho corretamente e aguarde até que o fornecedor do driver JDBC o conserte para que você possa atualizar o driver. Por outro lado, você não deve soltar um driver JDBC nos aplicativos da web /WEB-INF/lib, mas apenas nos servidores /lib. Se você ainda o mantiver em aplicativos da web /WEB-INF/lib, deverá registrá-lo manualmente e cancelar o registro usando a ServletContextListener.

  2. Faça o downgrade para o Tomcat 6.0.23 ou anterior para que você não seja incomodado com esses avisos. Mas silenciosamente manterá vazando memória. Não sei se é bom saber, afinal. Esse tipo de vazamento de memória é uma das principais causas dos OutOfMemoryErrorproblemas durante as implantações a quente do Tomcat.

  3. Mova o driver JDBC para a /libpasta do Tomcat e tenha uma fonte de dados em conjunto de conexões para gerenciar o driver. Observe que o DBCP interno do Tomcat não cancela o registro dos drivers corretamente ao fechar. Veja também o bug DBCP-322, que está fechado como WONTFIX. Você gostaria de substituir o DBCP por outro conjunto de conexões que esteja fazendo seu trabalho melhor que o DBCP. Por exemplo , HikariCP , BoneCP ou talvez Tomcat JDBC Pool .

BalusC
fonte
25
Este é um bom conselho. Não é aviso de um vazamento de memória, é um aviso de que Tomcat tomou alguma ação violenta para impedir um vazamento
matt b
49
Se a opção (1) é o caminho a seguir, por que o Tomcat os registra como SEVEROS? GRAVE para mim significa "paginar o administrador", não "ignorar".
Peter Becker
7
Por que não fazer você mesmo - em vez de esperar que o Tomcat faça isso. Na minha opinião, não é tarefa do Tomcat limpar nosso código confuso. Veja minha resposta abaixo.
Sparkyspider 19/10/11
2
@sproketboy: Hein? Você estava atribuindo artefatos JDBC como campos de uma classe que por sua vez é armazenada na sessão HTTP?
BalusC
2
Eu acho que geralmente é a razão 3 (ter a biblioteca no wAR em oposição a lib).
Lapo 25/11
160

No método contextDestroyed () do ouvinte de contexto do servlet, cancele o registro manual dos drivers:

// 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
fonte
3
Funciona! javabeat.net/servletcontextlistener-example pode ajudar a implementar ouvinte contexto servlet
Vadim Zin4uk
17
Isso é potencialmente inseguro em um ambiente compartilhado, pois você pode não desejar cancelar o registro de todos os drivers JDBC disponíveis. Veja minha resposta para uma abordagem mais segura.
Daiscog 28/05
85

Embora o Tomcat cancele o registro forçado do driver JDBC para você, é uma boa prática limpar todos os recursos criados pelo seu webapp na destruição de contexto, caso você mude para outro contêiner de servlet que não faça as verificações de prevenção de vazamento de memória que o Tomcat faz.

No entanto, a metodologia de cancelamento de registro geral do motorista é perigosa. Alguns drivers retornados pelo DriverManager.getDrivers()método podem ter sido carregados pelo ClassLoader pai (ou seja, o carregador de classe do contêiner do servlet) e não pelo ClassLoader do contexto do aplicativo da web (por exemplo, eles podem estar na pasta lib do contêiner, não nos aplicativos da web e, portanto, compartilhados em todo o contêiner ) O cancelamento do registro desses afetará quaisquer outros aplicativos da web que possam estar usando-os (ou até o próprio contêiner).

Portanto, deve-se verificar se o ClassLoader de cada driver é o ClassLoader do webapp antes de cancelá-lo. Portanto, no método contextDestroyed () do seu 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
fonte
Não deveria ser if (cl.equals(driver.getClass().getClassLoader())) {?
user11153
6
@ user11153 Não, estamos verificando se é precisamente a mesma instância do ClassLoader, não se há duas instâncias separadas com o mesmo valor.
Daiscog 28/05
4
Embora os outros pareçam lidar corretamente com o problema em questão, eles causam problemas quando o arquivo war é excluído e depois substituído. Nesse caso, os drivers são cancelados o registro e nunca retornam - apenas uma reinicialização do Tomcat pode tirar você desse buraco. Esta solução evita esse inferno.
OldCurmudgeon
Aqui basicamente o mesmo, mas com o código de manipulação adicional do MySQL / MariaDB github.com/spring-projects/spring-boot/issues/2612
gavenkoa
2
Se você usar H2 ou PostgreSQL, isso fará com que o driver não seja registrado novamente após a recarga. Ambos os drivers mantêm um estado de registro interno que não é limpo se o driver for apenas cancelado o registro no DriverManager. Deixei um comentário mais detalhado em github.com/spring-projects/spring-boot/issues/…
Marcel Stör
26

Vejo esse problema surgir muito. Sim, o Tomcat 7 cancela o registro automaticamente, mas é REALMENTE assumindo o controle do seu código e uma boa prática de codificação? Certamente, você deseja saber que possui todo o código correto para fechar todos os seus objetos, desligar os threads do conjunto de conexões com o banco de dados e se livrar de todos os avisos. Eu certamente faço.

É assim que eu faço.

Etapa 1: registrar um ouvinte

web.xml

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

Etapa 2: implementar o ouvinte

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, sinta-se livre para comentar e / ou adicionar ...

sparkyspider
fonte
4
Mas DataSourcenão tem um closemétodo
Jim
9
deve ser implementado javax.servlet.ServletContextListener, não estende ApplicationContextListener?
egaga 23/03
2
Existe uma razão para a ordem dessas operações no método contextDestroyed? Por que você faz passo 1. antes de fazer o passo 2., onde initContext, envContexte datasourcenão são referenciados em tudo? Estou perguntando porque eu não entendo passo 3.
matthaeus
4
@matthaeus Acho que a Etapa 1. não é necessária, lookupparece que apenas alguns objetos não são necessários. O passo 3. é completamente inútil. Certamente não adiciona nenhuma segurança e parece algo que os iniciantes fariam sem entender como o GC funciona. Gostaria apenas de ir com stackoverflow.com/a/5315467/897024 e remover todos os drivers.
Kapex
4
@kapep Remover todos os drivers é perigoso, pois alguns podem ser compartilhados no contêiner. Veja minha resposta para uma abordagem que remove apenas os drivers carregados pelo ClassLoader do seu aplicativo da web.
Daiscog 28/05
14

Este é um problema puramente de registro / cancelamento de registro no driver do mysql ou no webapp-classloader do mysql. Copie o driver mysql na pasta lib do tomcats (para que seja carregado diretamente pelo jvm, não pelo tomcat) e a mensagem desaparecerá. Isso faz com que o driver mysql jdbc seja descarregado apenas no desligamento da JVM, e ninguém se importa com vazamentos de memória.

velho bastardo doente
fonte
2
isso não funciona ... Eu tentei copiar o driver jdbc, você disse: TOMCAT_HOME / lib / postgresql-9.0-801.jdbc4.jar - Tomcat 7.0 \ lib \ postgresql-9.0-801.jdbc4.jar sem resultados ...
FAjir
5
@Florito - você tem que removê-lo de seu web-apps WEB-INF / lib assim
Collin Peters
8

Se você estiver recebendo esta mensagem de uma guerra criada pelo Maven, altere o escopo do driver JDBC para fornecido e coloque uma cópia dela no diretório lib. Como isso:

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

Solução para implantações por aplicativo

Este é um ouvinte que escrevi para resolver o problema: ele detecta automaticamente se o driver se registrou e age de acordo.

Importante: ele deve ser usado SOMENTE quando o jar do driver é implantado no WEB-INF / lib , não no Tomcat / lib, como muitos sugerem, para que cada aplicativo possa cuidar de seu próprio driver e executar em um Tomcat intocado . É assim que deve ser o IMHO.

Basta configurar o ouvinte no seu web.xml antes de qualquer outro e aproveitar.

adicione próximo ao topo do web.xml :

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

salve 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
fonte
Dica: No Servlet 3.0, você pode anotar sua classe @WebListenere omitir a web.xmlconfiguração.
Basil Bourque
É verdade, apenas certifique-se de que ele seja atendido com uma prioridade suficientemente alta, para que ninguém precise usar o driver antes ou depois.
Andrea Ratto
6

Vou acrescentar a isso algo que encontrei nos fóruns do Spring. Se você mover o jar do driver JDBC para a pasta lib do tomcat, em vez de implementá-lo com o aplicativo da web, o aviso parecerá desaparecer. Posso confirmar que isso funcionou para mim

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

Collin Peters
fonte
1
Acho que isso oferece uma solução melhor - basta subclassificar o DatasourceManager e substituir o método close para adicionar o cancelamento de registro. O Spring o manipulará quando destruir seu contexto, e o Tomcat não fornecerá o log SEVERE, e você não precisará mover o driver JDBC para o diretório lib. Aqui está a msg orignal do fórum velho primavera
Adam
6

Eu descobri que implementar um método simples destroy () para cancelar o registro de qualquer driver JDBC funciona bem.

/**
 * 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
fonte
5
Isso é potencialmente inseguro em um ambiente compartilhado, pois você pode não desejar cancelar o registro de todos os drivers JDBC disponíveis. Veja minha resposta para uma abordagem mais segura. Além disso, isso realmente deve ser feito em um ServletContextListener, não em uma base por servlet, pois o driver JDBC é compartilhado em todos os seus servlets no aplicativo da web.
Daiscog 28/05
3

Para evitar esse vazamento de memória, simplesmente cancele o registro do driver no encerramento do 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>

Fonte que me inspirou para esta correção de bug.

O rei da bruxaria
fonte
2

Eu estava tendo um problema semelhante, mas, além disso, estava recebendo um erro do Java Heap Space sempre que modificava / salvava páginas JSP com o servidor Tomcat em execução, portanto, o contexto não era totalmente recarregado.

Minhas versões eram Apache Tomcat 6.0.29 e JDK 6u12.

A atualização do JDK para 6u21, conforme sugerido na seção Referências da URL http://wiki.apache.org/tomcat/MemoryLeakProtection, resolveu o problema do Java Heap Space (o contexto agora recarrega OK), embora o erro do driver JDBC ainda apareça.

Francisco Alvarado
fonte
0

Encontrei o mesmo problema com o Tomcat versão 6.026.

Eu usei o Mysql JDBC.jar na WebAPP Library e no TOMCAT Lib.

Para corrigir o problema acima, removendo o Jar da pasta lib do TOMCAT.

Então, o que eu entendo é que o TOMCAT está lidando com o vazamento de memória JDBC corretamente. Mas se o jar MYSQL Jdbc for duplicado no WebApp e no Tomcat Lib, o Tomcat poderá manipular o jar presente na pasta Lib do Tomcat.

Bharat
fonte
0

Eu enfrentei esse problema quando estava implantando meu aplicativo Grails na AWS. Esta é uma questão de driver org.h2 do driver padrão JDBC . Como você pode ver isso no Datasource.groovy dentro da sua pasta de configuração. Como você pode ver abaixo:

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

Comente essas linhas sempre que houver mencionado org.h2.Driver no arquivo datasource.groovy, se você não estiver usando esse banco de dados. Caso contrário, você deverá baixar o arquivo jar do banco de dados.

Obrigado .

PyDevSRS
fonte
0

Este erro ocorreu comigo em um aplicativo Grails com o driver JTDS 1.3.0 (SQL Server). O problema foi um logon incorreto no SQL Server. Depois de resolver esse problema (no SQL Server), meu aplicativo foi implantado corretamente no Tomcat. Dica: eu vi o erro no stacktrace.log

cantoni
fonte