メモリリークを防ぐために、JDBCドライバーは強制的に登録解除されました


325

Webアプリケーションを実行すると、このメッセージが表示されます。正常に実行されますが、シャットダウン中にこのメッセージが表示されます。

重大:WebアプリケーションはJBDCドライバー[oracle.jdbc.driver.OracleDriver]を登録しましたが、Webアプリケーションが停止したときに登録解除に失敗しました。メモリリークを防止するために、JDBCドライバーは強制的に登録解除されました。

助けてくれてありがとう。


回答:


301

バージョン6.0.24以降、Tomcatにはメモリリーク検出機能が同梱されており、APIを使用してWebアプリケーションの起動時に/WEB-INF/lib自動登録するJDBC 4.0互換ドライバーがWebアプリケーションにある場合、この種の警告メッセージが表示される可能性があります。webappのシャットダウン中に自動登録解除さませんでした。このメッセージは純粋に非公式であり、Tomcatはすでにそれに応じてメモリリーク防止アクションを実行しています。ServiceLoader

あなたは何ができますか?

  1. これらの警告は無視してください。Tomcatは正しく機能しています。実際のバグは、あなたのコードではなく、他の誰かのコード(問題のJDBCドライバー)にあります。Tomcatが正しく機能し、JDBCドライバーのベンダーが修正してドライバーをアップグレードできるようになるまで待ちます。一方、JDBCドライバーをwebappにドロップするの/WEB-INF/libではなく、サーバーにドロップする必要があります/lib。それをwebappに保持している場合は/WEB-INF/lib、を使用して手動で登録および登録解除する必要がありServletContextListenerます。

  2. これらの警告に煩わされないように、Tomcat 6.0.23以前にダウングレードしてください。しかし、それは静かにメモリリークを続けます。結局それを知っておくのが良いかどうかわからない。これらの種類のメモリリークは、Tomcatのホットデプロイメント時のOutOfMemoryError問題の背後にある主要な原因の1つです。

  3. JDBCドライバーをTomcatの/libフォルダーに移動し、ドライバーを管理するための接続プールデータソースを用意します。Tomcatの組み込みDBCPは、終了時にドライバを適切に登録解除しないことに注意してください。WONTFIXとしてクローズされているバグDBCP-322も参照してください。DBCPの代わりに、DBCPよりも機能が優れている別の接続プールにDBCPを置き換えたいと思います。たとえば、HikariCPBoneCP、またはTomcat JDBCプール


25
これは良いアドバイスです。これはメモリリークを警告するものではなく、Tomcatがリークを防ぐために強制的なアクションをとったという警告です
matt b

49
オプション(1)が適切な方法である場合、Tomcatがこれらを重大と記録するのはなぜですか?SEVERE to meとは、「無視」ではなく「ページ管理者」を意味します。
Peter Becker

7
Tomcatに期待するのではなく、自分でやってみませんか。私の意見では、厄介なコードをクリーンアップするのはTomcatの仕事ではありません。以下の私の答えを参照してください。
sparkyspider '19年

2
@sproketboy:え?JDBCアーティファクトを、HTTPセッションに格納されるクラスのフィールドとして割り当てましたか?
BalusC 2013年

2
通常は理由3(とは対照的に、ライブラリをwARに持っている)だと思いlibます。
ラポ2014年

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

3
できます!javabeat.net/servletcontextlistener-exampleは、サーブレットコンテキストリスナーの実装に役立つ場合があります
Vadim Zin4uk

17
使用可能なすべての JDBCドライバーの登録を解除たくない場合があるため、これは共有環境では潜在的に安全ではありません。より安全なアプローチについては、私の回答を参照しください。
daiscog 2014年

85

TomcatはJDBCドライバーを強制的に登録解除しますが、それでも、Tomcatが行うメモリリーク防止チェックを実行しない別のサーブレットコンテナーに移動する場合に備えて、コンテキスト破棄時にWebアプリケーションによって作成されたすべてのリソースをクリーンアップすることをお勧めします。

ただし、ブランケットドライバーの登録解除の方法論は危険です。DriverManager.getDrivers()メソッド によって返された一部のドライバーは、webappコンテキストのClassLoaderではなく、親ClassLoader(つまり、サーブレットコンテナーのクラスローダー)によって読み込まれた可能性があります(たとえば、webappではなくコンテナーのlibフォルダーにあるため、コンテナー全体で共有されている可能性があります) )。これらの登録を解除すると、それらを使用している可能性のある他のWebアプリケーション(またはコンテナー自体)に影響します。

したがって、登録を解除する前に、各ドライバのClassLoaderがWebアプリケーションのClassLoaderであることを確認する必要があります。したがって、ContextListenerの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())) {
user11153

6
@ user11153いいえ、値が等しい2つの個別のインスタンスではなく、まったく同じClassLoaderインスタンスであるかどうかをチェックしています。
daiscog 2014年

4
他の人は問題の問題を正しく処理しているように見えますが、warファイルを削除して置き換えると問題が発生します。その場合、ドライバーは登録解除されて戻ることはありません。Tomcatを再起動するだけで、その穴から抜け出すことができます。このソリューションはその地獄を回避します。
OldCurmudgeon 14

ここではほとんど同じですが、追加のMySQL / MariaDB処理コードgithub.com/spring-projects/spring-boot/issues/2612
gavenkoa

2
H2またはPostgreSQLを使用すると、リロード時にドライバーが再登録されなくなります。両方のドライバーは、ドライバーがから登録解除されただけではクリアされない内部登録状態を維持しDriverManagerます。私は、より詳細なコメント左github.com/spring-projects/spring-boot/issues/...
マルセルSTOR

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
しかしDataSourcecloseメソッドはありません
ジムは

9
ApplicationContextListenerを拡張するのではなく、javax.servlet.ServletContextListenerを実装する必要がありますか?
egaga 2013年

2
メソッドでこれらの操作を順序付ける理由はありcontextDestroyedますか?なぜあなたはどこ、ステップ2を実行する前に、手順1を行うのですかinitContextenvContextdatasourceすべてで参照されていませんか?私はステップ3を理解していないので、私は聞いてるのよ
matthaeus

4
@matthaeusステップ1は不要だと思います。不要なlookupオブジェクトを取得するだけのようです。ステップ3.はまったく役に立たない。これは確かに安全性を追加するものではなく、GCの仕組みを理解していない初心者が行うようなものです。私は単にstackoverflow.com/a/5315467/897024を使用して、すべてのドライバーを削除します。
kapex 2013年

4
@kapep一部のドライバーはコンテナー全体で共有される可能性があるため、すべてのドライバーを削除すると危険です。WebアプリケーションのClassLoaderによってロードされたドライバーのみを削除するアプローチについては、私の回答を参照しください。
daiscog 2014年

14

これは、mysqlのドライバまたはtomcats webapp-classloaderの純粋なドライバ登録/登録解除の問題です。mysqlドライバーをtomcats libフォルダーにコピーすると(tomcatによってではなく、jvmによって直接ロードされます)、メッセージは表示されなくなります。これにより、JVMのシャットダウン時にのみmysql jdbcドライバがアンロードされ、メモリリークについては誰も気にしません。


2
動作しません... jdbcドライバーをコピーしようとしましたが、次のように言われました:TOMCAT_HOME / lib / postgresql-9.0-801.jdbc4.jar-Tomcat 7.0 \ lib \ postgresql-9.0-801.jdbc4.jar結果なし...
FAjir 2011年

5
@Florito-WebアプリWEB-INF / libからも削除する必要があります
Collin Peters

8

Mavenビルドのwarからこのメッセージを受け取っている場合は、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

アプリごとの展開のためのソリューション

これは、問題を解決するために私が書いたリスナーです。ドライバーが自分自身を登録したかどうかを自動検出し、それに応じて動作します。

重要:各アプリケーションが独自のドライバーを処理して、手つかずのTomcatで実行できるように、ドライバーjarが Tomcat / libではなくWEB-INF / libデプロイされている場合にのみ使用することを意図しています。それは私見であるべき方法です。

リスナーを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");
        }

    }

}

ヒント:サーブレット3.0以降では、クラスに注釈を付けたり@WebListenerweb.xml構成を省略したりできます。
バジルブルク2016年

確かに、それが十分に高い優先度で選択されていることを確認してください。そうすれば、前後にドライバーを使用する必要がなくなります。
Andrea Ratto 2016

6

これに、Springフォーラムで見つけたものを追加します。JDBCドライバーjarをwebappでデプロイする代わりに、tomcat libフォルダーに移動すると、警告が消えるようです。これでうまくいったことを確認できます

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 dirに移動する必要はありません。これは、古い春のフォーラムからの元のメッセージです
Adam

6

JDBCドライバーの登録を解除する単純なdestroy()メソッドの実装がうまく機能していることがわかりました。

/**
 * 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ドライバーの登録を解除たくない場合があるため、これは共有環境では潜在的に安全ではありません。より安全なアプローチについては、私の回答を参照しください。また、JDBCドライバーはWebアプリケーション内のすべてのサーブレットで共有されるため、これはサーブレットごとではなく、ServletContextListenerで行う必要があります。
daiscog 2014年

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

同様の問題がありましたが、Tomcatサーバーが実行されている状態でJSPページを変更または保存すると、Javaヒープスペースエラーが発生したため、コンテキストが完全に再充電されませんでした。

私のバージョンはApache Tomcat 6.0.29とJDK 6u12でした。

URL http://wiki.apache.org/tomcat/MemoryLeakProtectionの参照セクションで提案されているようにJDKを6u21にアップグレードすると、JDBCドライバーエラーが表示されますが、Javaヒープスペースの問題が解決されました(コンテキストはリロードできます)。


0

Tomcatバージョン6.026でも同じ問題が見つかりました。

MySQL JDBC.jarをWebAPPライブラリとTOMCAT Libで使用しました。

TOMCAT libフォルダーからJarを削除して上記を修正します。

だから私が理解しているのは、TOMCATがJDBCメモリリークを適切に処理しているということです。ただし、MYSQL Jdbc jarがWebAppとTomcat Libで重複している場合、TomcatはTomcat Libフォルダーにあるjarしか処理できません。


0

GrailsアプリケーションをAWSにデプロイしているときにこの問題に直面しました。これは、JDBCデフォルトドライバーorg.h2ドライバーの問題です。ご覧のとおり、これは構成フォルダー内のDatasource.groovyにあります。あなたが下に見ることができるように:

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

そのデータベースを使用していない場合は、datasource.groovyファイルのorg.h2.Driverに記載されている箇所にコメントを付けてください。それ以外の場合は、そのデータベースjarファイルをダウンロードする必要があります。

感謝します。


0

このエラーは、JTDS Driver 1.3.0(SQL Server)を使用するGrailsアプリケーションで発生しました。問題はSQL Serverでの不正なログインでした。この問題を(SQL Serverで)解決した後、私のアプリはTomcatに正しくデプロイされました。ヒント:stacktrace.logでエラーを確認しました

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.