Щоб запобігти витоку пам’яті, драйвер JDBC був насильно незареєстрований


325

Я отримую це повідомлення під час запуску веб-програми. Це працює добре, але я отримую це повідомлення під час відключення.

СЕВЕР: Веб-програма зареєструвала драйвер JBDC [oracle.jdbc.driver.OracleDriver], але не вдалося скасувати реєстрацію, коли веб-додаток було зупинено. Щоб запобігти витоку пам’яті, драйвер JDBC був насильно незареєстрований.

Будь-яка допомога вдячна.


4
Може бути дублікатом stackoverflow.com/questions/2604630 / ...
skaffman

Відповіді:


301

Починаючи з версії 6.0.24, Tomcat постачається з функцією виявлення витоку пам’яті , що, в свою чергу, може призвести до подібного роду попереджувальних повідомлень, коли в веб-переглядачі веб-сайту є сумісний драйвер JDBC 4.0, /WEB-INF/libякий автоматично реєструється під час запуску webapp за допомогою 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 і створіть джерело даних, підключений для управління драйвером. Зауважте, що вбудований Tomcat DBCP не реєструє драйвери належним чином при закритті. Дивіться також помилку DBCP-322, яка закрита як WONTFIX. Ви б хотіли замінити DBCP іншим пулом з'єднань, який робить свою роботу краще, ніж DBCP. Наприклад, HikariCP , BoneCP або, можливо, басейн Tomcat JDBC .


25
Це хороша порада. Це не попередження про витік пам'яті, це попередження про те, що Tomcat вчинив насильницькі дії для запобігання витоку
мат b

49
Якщо варіант (1) - це шлях, чому Tomcat записує їх як СЕВЕРЕ? SEVERE для мене означає "сторінка адміністратора", а не "ігнорувати".
Пітер Бекер

7
Чому б не зробити це самостійно - а не сподіватися, що це робитиме Tomcat. На мою думку, справа в Томкаті не в тому, щоб прибрати наш безладний код. Дивіться мою відповідь нижче.
sparkyspider

2
@sproketboy: Так? Ви призначали артефакти JDBC як поля класу, який, в свою чергу, зберігається в сесії HTTP?
BalusC

2
Я думаю, це зазвичай причина 3 (наявність бібліотеки у війні на відміну від lib).
lapo

160

У вашому контексті прослуховувача сервлетів контекстDestroyed () вручну скасуйте драйвери:

// 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
Це працює! javabeat.net/servletcontextlistener-example може допомогти реалізувати слухача контексту сервлетів
Вадим Зін4ук

17
Це може бути небезпечним у спільному середовищі, оскільки ви, можливо, не захочете скасувати реєстрацію всіх наявних драйверів JDBC. Дивіться мою відповідь щодо більш безпечного підходу.
Дайског

85

Хоча Tomcat примусово скасовує драйвер JDBC для вас, тим не менше є хорошою практикою очищати всі ресурси, створені вашим веб-сервером для знищення контексту, якщо ви переходите до іншого контейнера сервлетів, який не робить перевірки запобігання витоку пам'яті, які робить Tomcat.

Однак методологія зняття з реєстрації водіїв ковдри небезпечна. Деякі драйвери, повернуті DriverManager.getDrivers()методом, можуть бути завантажені батьківським ClassLoader (тобто, завантажувачем класу контейнера сервлета), а не ClassLoader контексту webapp (наприклад, вони можуть знаходитися в папці lib контейнера, а не в webapp, і тому поділяються по всьому контейнеру ). Скасування їх реєстрації вплине на будь-які інші веб-карти, які можуть використовувати їх (або навіть сам контейнер).

Тому слід перевірити, що ClassLoader для кожного драйвера - це ClassLoader веб-сайту Webapp, перш ніж скасувати його реєстрацію. Отже, у вашому методі ContextListener’s contextDestroyed ():

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

Не повинно бути if (cl.equals(driver.getClass().getClassLoader())) {?
користувач11153

6
@ user11153 Ні, ми перевіряємо, чи це точно той самий екземпляр ClassLoader, ні, якщо це два окремих екземпляри, які мають однакові значення.
дайског

4
Хоча інші, здається, правильно вирішують проблему, яка виникає, вони створюють проблеми, коли файл війни видалено та замінено. У такому випадку драйвери відписуються і ніколи не повертаються - тільки перезапуск Tomcat може вивести вас із цієї діри. Це рішення дозволяє уникнути цього пекла.
OldCurmudgeon

Тут здебільшого те саме, але з додатковим кодом обробки MySQL / MariaDB github.com/spring-projects/spring-boot/issues/2612
gavenkoa

2
Якщо ви використовуєте H2 або PostgreSQL, це призводить до того, що драйвер знову не реєструється після перезавантаження. Обидва драйвери підтримують стан внутрішньої реєстрації, який не видаляється, якщо драйвер просто скасовано з DriverManager. Більш детальний коментар я залишив на сайті github.com/spring-projects/spring-boot/isissue/…
Marcel Stör

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

}

Будь ласка, коментуйте та / або додайте ...


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.
дайског

14

Це суто проблема з реєстрацією / зняттям реєстрації водіїв у драйвері mysql`s або 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
Collin Peters

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>

8

Рішення для розгортання програми

Це слухач, якого я написав, щоб вирішити проблему: він автоматично виявляє, якщо драйвер зареєструвався і діє відповідно

Важливо: він призначений використовуватись ТОЛЬКІ, коли банку драйверів розгорнута в 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 у папку tomcat lib, замість того, щоб розгортати її за допомогою веб-сайту, попередження, схоже, зникає. Я можу підтвердити, що це працювало на мене

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


1
Я думаю, що це пропонує краще рішення - просто підкласируйте свій DatasourceManager та замініть метод закриття, щоб додати зняття з реєстрації. Тоді Spring почне справлятися з цим, коли він руйнує його контекст, і Tomcat не видасть вам журнал SEVERE, і вам не доведеться переміщувати драйвер JDBC у dir lib. Ось оригінальний сервер із старого форуму весни
Адам,

6

Я виявив, що реалізація простого методу знищення () для дереєстрації будь-яких драйверів 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 ділиться на всі ваші сервлети у вашому веб-сервері.
Дайског

3

Щоб запобігти цьому витоку пам'яті, просто скасуйте драйвер при відключенні контексту.

пом.хмл

<?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 Heap Space (контекст зараз перезавантажується ОК), хоча помилка драйвера 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 jar-файл.

Дякую .


0

Ця помилка сталася зі мною в додатку Grails з драйвером JTDS 1.3.0 (SQL Server). Проблема полягала в неправильному вході в SQL Server. Після вирішення цієї проблеми (на SQL Server) мій додаток було правильно розгорнуто в Tomcat. Порада. Я побачив помилку в stacktrace.log

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.