Чтобы предотвратить утечку памяти, драйвер JDBC был принудительно незарегистрирован

325

Я получаю это сообщение при запуске своего веб-приложения. Он работает нормально, но я получаю это сообщение во время выключения.

SEVERE: веб-приложение зарегистрировало драйвер JBDC [oracle.jdbc.driver.OracleDriver], но не удалось отменить его регистрацию при остановке веб-приложения. Чтобы предотвратить утечку памяти, драйвер JDBC был принудительно незарегистрирован.

Любая помощь приветствуется.

мона
источник
4
Может быть дубликатом stackoverflow.com/questions/2604630/…
skaffman

Ответы:

301

Начиная с версии 6.0.24, Tomcat поставляется с функцией обнаружения утечки памяти , которая, в свою очередь, может приводить к появлению таких предупреждений, когда в веб-приложении есть драйвер, совместимый с JDBC 4.0, /WEB-INF/libкоторый автоматически регистрируется во время запуска веб-приложения с помощью ServiceLoaderAPI , но который не авторегистрация себя во время закрытия веб-приложения. Это сообщение является чисто неформальным, Tomcat уже предпринял соответствующие меры по предотвращению утечки памяти.

Что ты можешь сделать?

  1. Игнорируйте эти предупреждения. Tomcat делает свою работу правильно. Фактическая ошибка в чужом коде (рассматриваемый драйвер JDBC), а не в вашем. Будьте счастливы, что Tomcat выполнил свою работу должным образом, и подождите, пока поставщик драйверов JDBC не исправит это, чтобы вы могли обновить драйвер. С другой стороны, вы не должны удалять драйвер JDBC в веб-приложениях /WEB-INF/lib, а только в серверах /lib. Если вы по-прежнему храните его в веб-приложении /WEB-INF/lib, вам следует вручную зарегистрировать и отменить регистрацию с помощью ServletContextListener.

  2. Вернитесь к Tomcat 6.0.23 или более поздней версии, чтобы вас не беспокоили эти предупреждения. Но это будет молча продолжать утекать память. Не уверен, хорошо ли это знать в конце концов. Такие утечки памяти являются одной из основных причин OutOfMemoryErrorпроблем, возникающих во время горячих развертываний Tomcat.

  3. Переместите драйвер JDBC в /libпапку Tomcat и получите источник данных пула соединений для управления драйвером. Обратите внимание, что встроенный DBCP Tomcat не отменяет регистрацию драйверов при закрытии. Смотрите также ошибку DBCP-322, которая закрывается как WONTFIX. Вы бы предпочли заменить DBCP другим пулом соединений, который выполняет свою работу лучше, чем DBCP. Например, HikariCP , BoneCP или, возможно, Tomcat JDBC Pool .

BalusC
источник
25
Это хороший совет. Это не предупреждение об утечке памяти, это предупреждение о том, что Tomcat предпринял некоторые принудительные действия для предотвращения утечки
matt b
49
Если вариант (1) подходит, почему Tomcat регистрирует их как SEVERE? SEVERE для меня означает «страница администратора», а не «игнорировать».
Питер Беккер
7
Почему бы не сделать это самостоятельно, а не ожидать, что Tomcat сделает это. На мой взгляд, работа Tomcat не в том, чтобы убирать наш грязный код. Смотрите мой ответ ниже.
sparkyspider
2
@sproketboy: А? Вы назначали артефакты JDBC как поля класса, который, в свою очередь, хранится в сеансе HTTP?
BalusC
2
Я предполагаю, что это обычно причина 3 (наличие библиотеки в WAR вместо lib).
Lapo
160

В методе contextDestroyed () прослушивателя контекста сервлета вручную отмените регистрацию драйверов:

// 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
источник
3
Оно работает! javabeat.net/servletcontextlistener-example может помочь реализовать прослушиватель контекста сервлета
Вадим Зин4ук,
17
Это потенциально небезопасно в общей среде, так как вы можете не захотеть отменять регистрацию всех доступных драйверов JDBC. Смотрите мой ответ для более безопасного подхода.
daiscog
85

Хотя Tomcat принудительно отменяет регистрацию драйвера JDBC для вас, тем не менее, рекомендуется очистить все ресурсы, созданные вашим веб-приложением, при уничтожении контекста в случае, если вы переместитесь в другой контейнер сервлета, который не выполняет проверки предотвращения утечки памяти, как это делает Tomcat.

Однако методика отмены регистрации общего драйвера опасна. Некоторые драйверы, возвращаемые DriverManager.getDrivers()методом, возможно, были загружены родительским ClassLoader (т. Е. Загрузчиком классов контейнера сервлета), а не ClassLoader контекста веб-приложения (например, они могут находиться в папке lib контейнера, а не в веб-приложении, и, следовательно, совместно использоваться по всему контейнеру). ). Отмена регистрации повлияет на любые другие веб-приложения, которые могут их использовать (или даже на сам контейнер).

Следовательно, следует проверить, что ClassLoader для каждого драйвера является ClassLoader веб-приложения, прежде чем отменять его регистрацию. Итак, в вашем методе contextDistener () вашего 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
источник
Не должно быть if (cl.equals(driver.getClass().getClassLoader())) {?
user11153
6
@ user11153 Нет, мы проверяем, является ли это точно один и тот же экземпляр ClassLoader, а не если это два отдельных экземпляра, которые равны по значению.
daiscog
4
Хотя другие, кажется, правильно справляются с рассматриваемой проблемой, они вызывают проблемы, когда файл war удаляется, а затем заменяется. В этом случае драйверы отменяются и никогда не возвращаются - только перезагрузка Tomcat может вытащить вас из этой дыры. Это решение позволяет избежать этого ада.
OldCurmudgeon
Здесь в основном то же самое, но с дополнительным кодом обработки MySQL / MariaDB github.com/spring-projects/spring-boot/issues/2612
gavenkoa
2
Если вы используете H2 или PostgreSQL, это приведет к тому, что драйвер не будет снова зарегистрирован после перезагрузки. Оба драйвера поддерживают внутреннее состояние регистрации, которое не очищается, если драйвер просто отменен из DriverManager. Я оставил более подробный комментарий по адресу github.com/spring-projects/spring-boot/issues/…
Марсель Стёр
26

Я вижу, что эта проблема часто возникает. Да, Tomcat 7 автоматически отменяет регистрацию, но действительно ли он берет под свой контроль ваш код и хорошую практику кодирования? Конечно, вы хотите знать, что у вас есть весь правильный код, чтобы закрыть все ваши объекты, закрыть потоки пулов соединений с базой данных и избавиться от всех предупреждений. Я конечно делаю.

Вот как я это делаю.

Шаг 1: Зарегистрируйте слушателя

web.xml

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

Шаг 2: внедрить слушателя

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

}

Пожалуйста, не стесняйтесь комментировать и / или добавлять ...

sparkyspider
источник
4
Но DataSourceнет closeметода
Джим
9
он должен быть реализует javax.servlet.ServletContextListener, а не расширяет ApplicationContextListener?
egaga
2
Есть ли причина для порядка этих операций в методе contextDestroyed? Почему вы делаете шаг 1. прежде чем делать шаг 2., где initContext, envContextи datasourceне ссылаются на все? Я спрашиваю, потому что я не понимаю, шаг 3.
Матфей
4
@matthaeus Я думаю, что Шаг 1. не является необходимым, lookupкажется, просто получает некоторые объекты, которые вам не нужны. Шаг 3. совершенно бесполезен. Это, конечно, не добавляет никакой безопасности и похоже на то, что сделали бы новички, не понимающие, как работает GC. Я бы просто пошел с stackoverflow.com/a/5315467/897024 и удалил все драйверы.
kapex
4
@kapep Удаление всех драйверов опасно, так как некоторые из них могут быть общими для всего контейнера. Смотрите мой ответ для подхода, который удаляет только драйверы, загруженные ClassLoader вашего веб-приложения.
daiscog
14

Это чисто проблема регистрации / отмены регистрации драйвера в драйвере mysql или в tomcats webapp-classloader. Скопируйте драйвер mysql в папку lib tomcats (чтобы он загружался непосредственно jvm, а не tomcat), и сообщение исчезнет. Это делает драйвер mysql jdbc выгруженным только при выключении JVM, и никто не заботится об утечках памяти.

старый больной ублюдок
источник
2
это не работает ... Я попытался скопировать драйвер jdbc, вы сказали: TOMCAT_HOME / lib / postgresql-9.0-801.jdbc4.jar - Tomcat 7.0 \ lib \ postgresql-9.0-801.jdbc4.jar безрезультатно ...
FAjir
5
@Florito - вы также должны удалить его из веб-приложений WEB-INF / lib
Коллин Питерс
8

Если вы получаете это сообщение от встроенной войны Maven, измените область действия драйвера JDBC на предоставленную и поместите его копию в каталог lib. Как это:

<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
источник
8

Решение для развертывания приложений

Это слушатель, который я написал для решения проблемы: он автоматически определяет, зарегистрировался ли драйвер и действует соответственно.

Важный: это предназначено, чтобы использоваться ТОЛЬКО, когда jar драйвера развернут в WEB-INF / lib , а не в Tomcat / lib, как многие предлагают, чтобы каждое приложение могло позаботиться о своем собственном драйвере и работать на нетронутом Tomcat , Именно так и должно быть ИМХО.

Просто настройте прослушиватель в вашем web.xml перед любым другим и наслаждайтесь.

добавить в верхней части web.xml :

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

сохранить как 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");
        }

    }

}
Андреа Ратто
источник
Подсказка: Начиная с Servlet 3.0, вы можете аннотировать свой класс @WebListenerи не указывать web.xmlконфигурацию.
Василий Бурк
Правда, просто убедитесь, что он выбран с достаточно высоким приоритетом, чтобы никому не приходилось использовать драйвер до или после.
Андреа Ратто
6

Я добавлю к этому то, что я нашел на весенних форумах. Если вы переместите свой драйвер JDBC-драйвера в папку lib tomcat, а не развернете его с помощью веб-приложения, предупреждение, похоже, исчезнет. Я могу подтвердить, что это сработало для меня

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

Коллин Питерс
источник
1
Я думаю, что это предлагает лучшее решение - просто создайте подкласс вашего DatasourceManager и переопределите метод close, чтобы добавить отмену регистрации. Тогда Spring будет обрабатывать его, когда он разрушит свой контекст, и Tomcat не выдаст вам журнал SEVERE, и вам не нужно будет перемещать драйвер JDBC в каталог lib. Вот оригинальное сообщение со старого весеннего форума
Адам
6

Я обнаружил, что реализация простого метода destroy () для отмены регистрации любых драйверов JDBC прекрасно работает.

/**
 * 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
Это потенциально небезопасно в общей среде, так как вы можете не захотеть отменять регистрацию всех доступных драйверов JDBC. Смотрите мой ответ для более безопасного подхода. Кроме того, это действительно должно быть сделано в ServletContextListener, а не для каждого сервлета, так как ваш драйвер JDBC используется всеми сервлетами в вашем веб-приложении.
daiscog
3

Чтобы предотвратить эту утечку памяти, просто отмените регистрацию драйвера при закрытии контекста.

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>

Источник, который вдохновил меня на исправление этой ошибки.

Король-Мастер
источник
2

У меня была похожая проблема, но, кроме того, я получал ошибку Java Heap Space каждый раз, когда я изменял / сохранял страницы JSP с запущенным сервером Tomcat, поэтому контекст не был полностью перезагружен.

Моими версиями были Apache Tomcat 6.0.29 и JDK 6u12.

Обновление JDK до 6u21, как предлагается в разделе « Ссылки » URL-адреса http://wiki.apache.org/tomcat/MemoryLeakProtection, решило проблему пространства кучи Java (контекст теперь перезагружается), хотя ошибка драйвера JDBC по-прежнему появляется.

Франсиско Альварадо
источник
0

Я обнаружил ту же проблему с Tomcat версии 6.026.

Я использовал Mysql JDBC.jar в библиотеке WebAPP, а также в TOMCAT Lib.

Чтобы исправить вышеперечисленное, удалив Jar из папки lib TOMCAT.

Так что я понимаю, что TOMCAT правильно обрабатывает утечку памяти JDBC. Но если jar-файл MYSQL Jdbc дублируется в WebApp и Tomcat Lib, Tomcat сможет обрабатывать только jar-файл, присутствующий в папке Tomcat Lib.

Бхарат
источник
0

Я столкнулся с этой проблемой, когда развертывал свое приложение Grails на AWS. Это вопрос драйвера JDBC по умолчанию драйвер org.h2 . Как вы можете видеть это в Datasource.groovy внутри вашей папки конфигурации. Как вы можете видеть ниже:

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

Прокомментируйте те строки, где есть упоминание org.h2.Driver в файле datasource.groovy , если вы не используете эту базу данных. В противном случае вы должны загрузить этот файл jar базы данных.

Спасибо .

PyDevSRS
источник
0

Эта ошибка произошла со мной в приложении Grails с драйвером JTDS 1.3.0 (SQL Server). Проблема заключалась в неправильном входе в SQL Server. После решения этой проблемы (в SQL Server) мое приложение было правильно развернуто в Tomcat. Совет: я видел ошибку в stacktrace.log

Кантони
источник