Untuk mencegah kebocoran memori, Driver JDBC telah secara paksa tidak terdaftar


325

Saya mendapatkan pesan ini ketika saya menjalankan aplikasi web saya. Ini berjalan dengan baik tetapi saya mendapatkan pesan ini selama shutdown

SEVERE: Aplikasi web mendaftarkan driver JBDC [oracle.jdbc.driver.OracleDriver] tetapi gagal membatalkan registrasi ketika aplikasi web dihentikan. Untuk mencegah kebocoran memori, Driver JDBC telah secara paksa tidak terdaftar.

Setiap bantuan dihargai.


Jawaban:


301

Sejak versi 6.0.24, Tomcat hadir dengan fitur pendeteksi kebocoran memori , yang pada gilirannya dapat menyebabkan pesan peringatan semacam ini ketika ada driver yang kompatibel dengan JDBC 4.0 di webapp /WEB-INF/libyang secara otomatis mendaftar sendiri saat startup webapp menggunakan ServiceLoaderAPI , tetapi tidak otomatis mendaftar sendiri selama penutupan webapp. Pesan ini murni informal, Tomcat telah mengambil tindakan pencegahan kebocoran memori yang sesuai.

Apa yang bisa kau lakukan?

  1. Abaikan peringatan itu. Tomcat melakukan tugasnya dengan benar. Bug yang sebenarnya ada dalam kode orang lain (driver JDBC yang dimaksud), bukan milik Anda. Senang bahwa Tomcat melakukan tugasnya dengan benar dan tunggu sampai vendor driver JDBC memperbaikinya sehingga Anda dapat memutakhirkan driver. Di sisi lain, Anda tidak seharusnya menjatuhkan driver JDBC di webapp /WEB-INF/lib, tetapi hanya di server /lib. Jika Anda masih menyimpannya di webapp /WEB-INF/lib, maka Anda harus mendaftar dan membatalkan pendaftaran secara manual menggunakan a ServletContextListener.

  2. Turunkan versi ke Tomcat 6.0.23 atau lebih lama sehingga Anda tidak akan terganggu dengan peringatan itu. Tapi itu akan secara diam-diam membocorkan ingatan. Tidak yakin apakah itu baik untuk diketahui. Kebocoran memori semacam itu adalah salah satu penyebab utama di balik OutOfMemoryErrormasalah selama masa kerja hotdot Tomcat.

  3. Pindahkan driver JDBC ke /libfolder Tomcat dan memiliki koneksi kumpulan data sumber daya untuk mengelola driver. Perhatikan bahwa DBCP bawaan Tomcat tidak membatalkan registrasi driver dengan benar saat tutup. Lihat juga bug DBCP-322 yang ditutup sebagai WONTFIX. Anda lebih suka mengganti DBCP dengan kumpulan koneksi lain yang melakukan tugasnya lebih baik daripada DBCP. Misalnya HikariCP , BoneCP , atau mungkin Tomcat JDBC Pool .


25
Ini saran yang bagus. Ini bukan peringatan kebocoran memori, ini peringatan bahwa Tomcat mengambil tindakan paksa untuk mencegah kebocoran
matt b

49
Jika opsi (1) adalah cara untuk pergi, mengapa Tomcat mencatatnya sebagai SEVERE? SEVERE bagi saya berarti "halaman admin", bukan "abaikan".
Peter Becker

7
Mengapa tidak melakukannya sendiri - daripada berharap Tomcat melakukannya. Menurut pendapat saya, bukan tugas Tomcat untuk membersihkan kode berantakan kami. Lihat jawaban saya di bawah ini.
sparkyspider

2
@sproketboy: Hah? Apakah Anda menetapkan artefak JDBC sebagai bidang kelas yang pada gilirannya disimpan dalam sesi HTTP?
BalusC

2
Saya kira itu biasanya alasan 3 (memiliki perpustakaan di wAR sebagai lawan lib).
Lapo

160

Dalam metode konteks konteks pendengar servlet Anda (), deregister driver secara manual:

// 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
Berhasil! javabeat.net/servletcontextlistener-example dapat membantu mengimplementasikan pendengar konteks servlet
Vadim Zin4uk

17
Ini berpotensi tidak aman di lingkungan bersama karena Anda mungkin tidak ingin membatalkan pendaftaran semua driver JDBC yang tersedia. Lihat jawaban saya untuk pendekatan yang lebih aman.
daiscog

85

Meskipun Tomcat secara paksa membatalkan pendaftaran driver JDBC untuk Anda, tetap merupakan praktik yang baik untuk membersihkan semua sumber daya yang dibuat oleh aplikasi web Anda pada penghancuran konteks jika Anda pindah ke wadah servlet lain yang tidak melakukan pemeriksaan pencegahan kebocoran memori yang dilakukan Tomcat.

Namun, metodologi deregistrasi blanket driver berbahaya. Beberapa driver yang dikembalikan oleh DriverManager.getDrivers()metode ini mungkin telah dimuat oleh induk ClassLoader (mis., Classloader servlet container) bukan ClassLoader konteks webapp (misalnya, mereka mungkin ada di folder lib kontainer, bukan webapp, dan karenanya dibagikan di seluruh wadah ). Membatalkan pendaftaran ini akan memengaruhi webapp lainnya yang mungkin menggunakannya (atau bahkan wadah itu sendiri).

Oleh karena itu, orang harus memeriksa apakah ClassLoader untuk setiap driver adalah ClassLoader webapp sebelum membatalkan pendaftaran. Jadi, dalam metode ContextListener contextDestroyed () Anda:

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

Tidak seharusnya if (cl.equals(driver.getClass().getClassLoader())) {?
user11153

6
@ user11153 Tidak, kami memeriksa apakah itu persisnya instance ClassLoader yang sama, tidak jika itu adalah dua instance terpisah yang nilainya sama.
daiscog

4
Meskipun yang lain tampaknya benar menangani masalah yang dimaksud, mereka menyebabkan masalah ketika file perang dihapus dan kemudian diganti. Dalam hal itu driver tidak terdaftar dan tidak pernah kembali - hanya restart Tomcat yang dapat mengeluarkan Anda dari lubang itu. Solusi ini menghindari neraka itu.
OldCurmudgeon

Di sini sebagian besar sama tetapi dengan tambahan kode penanganan MySQL / MariaDB github.com/spring-projects/spring-boot/issues/2612
gavenkoa

2
Jika Anda menggunakan H2 atau PostgreSQL ini mengakibatkan driver tidak terdaftar lagi saat memuat ulang. Kedua driver mempertahankan status registrasi internal yang tidak dihapus jika pengemudi hanya keluar dari daftar DriverManager. Saya meninggalkan komentar yang lebih terperinci di github.com/spring-projects/spring-boot/issues/…
Marcel Stör

26

Saya melihat masalah ini banyak muncul. Ya, Tomcat 7 tidak secara otomatis membatalkan pendaftaran, tetapi apakah BENAR-BENAR mengendalikan kode Anda dan praktik pengkodean yang baik? Tentunya ANDA ingin tahu bahwa Anda memiliki semua kode yang benar untuk menutup semua objek Anda, mematikan utas koneksi database, dan menyingkirkan semua peringatan. Tentu saja saya lakukan.

Beginilah cara saya melakukannya.

Langkah 1: Daftarkan Pendengar

web.xml

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

Langkah 2: Terapkan Pendengar

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

}

Silakan berkomentar dan / atau tambahkan ...


4
Tetapi DataSourceTIDAK memiliki closemetode
Jim

9
haruskah itu mengimplementasikan javax.servlet.ServletContextListener, tidak memperpanjang ApplicationContextListener?
egaga

2
Apakah ada alasan untuk urutan operasi tersebut dalam metode ini contextDestroyed? Mengapa Anda melakukan langkah 1. sebelum melakukan langkah 2., di mana initContext, envContextdan datasourcetidak dirujuk sama sekali? Saya bertanya karena saya tidak mengerti langkah 3.
matthaeus

4
@matthaeus Saya pikir Langkah 1. tidak perlu, lookuptampaknya hanya mendapatkan beberapa objek yang tidak Anda butuhkan. Langkah 3. sama sekali tidak berguna. Ini tentu saja tidak menambah keamanan dan sepertinya sesuatu yang pemula akan lakukan yang tidak mengerti cara kerja GC. Saya hanya akan pergi dengan stackoverflow.com/a/5315467/897024 dan menghapus semua driver.
kapex

4
@kapep Menghapus semua driver berbahaya karena beberapa mungkin dibagikan di seluruh wadah. Lihat jawaban saya untuk pendekatan yang hanya menghapus driver yang dimuat oleh ClassLoader webapp Anda.
daiscog

14

Ini murni masalah registrasi / larangan registrasi di mysql`s driver atau tomcats webapp-classloader. Salin driver mysql ke folder lib tomcats (jadi itu dimuat oleh jvm secara langsung, bukan oleh tomcat), dan pesan akan hilang. Itu membuat driver jdbc mysql diturunkan hanya pada saat shutdown JVM, dan tidak ada yang peduli dengan kebocoran memori saat itu.


2
itu tidak berfungsi ... Saya mencoba untuk menyalin driver jdbc yang telah Anda katakan: TOMCAT_HOME / lib / postgresql-9.0-801.jdbc4.jar - Tomcat 7.0 \ lib \ postgresql-9.0-801.jdbc4.jar tanpa hasil ...
FAjir

5
@Florito - Anda harus menghapusnya dari aplikasi web Anda WEB-INF / lib juga
Collin Peters

8

Jika Anda mendapatkan pesan ini dari perang bawaan Maven, ubah cakupan driver JDBC yang disediakan, dan letakkan salinannya di direktori lib. Seperti ini:

<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

Solusi untuk penerapan per-aplikasi

Ini adalah pendengar yang saya tulis untuk memecahkan masalah: ia mendeteksi secara otomatis jika driver telah mendaftarkan dirinya dan bertindak sesuai dengannya.

Penting: ini dimaksudkan untuk HANYA digunakan ketika toples driver digunakan di WEB-INF / lib , bukan di Tomcat / lib, seperti yang disarankan banyak orang, sehingga setiap aplikasi dapat merawat drivernya sendiri dan menjalankan Tomcat yang tidak tersentuh. . Begitulah seharusnya IMHO.

Konfigurasikan pendengar di web.xml Anda sebelum yang lain dan selamat menikmati.

tambahkan di dekat bagian atas web.xml :

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

simpan sebagai 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");
        }

    }

}

Tip: Pada Servlet 3.0, Anda dapat membuat anotasi kelas Anda dengan @WebListenerdan menghilangkan web.xmlkonfigurasi.
Basil Bourque

Benar, pastikan saja itu diambil dengan prioritas yang cukup tinggi, sehingga tidak ada yang perlu menggunakan driver sebelum atau sesudah.
Andrea Ratto

6

Saya akan menambahkan ini sesuatu yang saya temukan di forum Spring. Jika Anda memindahkan toples driver JDBC Anda ke folder tomcat lib, alih-alih menggunakan dengan aplikasi web Anda, peringatan itu tampaknya hilang. Saya dapat mengkonfirmasi bahwa ini berhasil untuk saya

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


1
Saya pikir ini menawarkan solusi yang lebih baik - cukup subkelas DatasourceManager Anda dan ganti metode tutup untuk menambahkan deregistrasi. Kemudian Spring akan menanganinya ketika itu menghancurkan konteksnya, dan Tomcat tidak akan memberi Anda log SEVERE, dan Anda tidak harus memindahkan driver JDBC Anda ke lib dir. Ini pesan orignal dari forum musim semi yang lama
Adam

6

Saya menemukan bahwa menerapkan metode destroy () sederhana untuk membatalkan registrasi driver JDBC bekerja dengan baik.

/**
 * 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
Ini berpotensi tidak aman di lingkungan bersama karena Anda mungkin tidak ingin membatalkan pendaftaran semua driver JDBC yang tersedia. Lihat jawaban saya untuk pendekatan yang lebih aman. Juga, ini benar-benar harus dilakukan dalam ServletContextListener, bukan berdasarkan per-servlet karena driver JDBC Anda dibagikan di semua servlet Anda di aplikasi web Anda.
daiscog

3

Untuk mencegah kebocoran memori ini, cukup deregister driver pada saat shutdown konteks.

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>

Sumber yang menginspirasi saya untuk perbaikan bug ini.


2

Saya mengalami masalah yang sama, tetapi juga saya mendapatkan kesalahan Java Heap Space setiap kali saya memodifikasi / menyimpan halaman JSP dengan server Tomcat berjalan, oleh karena itu konteksnya tidak sepenuhnya diisi ulang.

Versi saya adalah Apache Tomcat 6.0.29 dan JDK 6u12.

Meningkatkan JDK ke 6u21 seperti yang disarankan di bagian Referensi URL http://wiki.apache.org/tomcat/MemoryLeakProtection memecahkan masalah Java Heap Space (konteks sekarang reload OK) meskipun kesalahan Driver JDBC masih muncul.


0

Saya menemukan masalah yang sama dengan Tomcat versi 6.026.

Saya menggunakan JDBC.jar Mysql di Perpustakaan WebAPP serta di TOMCAT Lib.

Untuk memperbaiki di atas dengan menghapus Jar dari folder lib TOMCAT.

Jadi yang saya pahami adalah bahwa TOMCAT menangani kebocoran memori JDBC dengan benar. Tetapi jika toples MYSQL Jdbc digandakan di WebApp dan Tomcat Lib, Tomcat hanya akan dapat menangani toples yang ada di folder Tomcat Lib.


0

Saya menghadapi masalah ini ketika saya menggunakan aplikasi Grails di AWS. Ini adalah masalah driver JDBC default driver org.h2 . Seperti yang Anda lihat di Datasource.groovy di dalam folder konfigurasi Anda. Seperti yang Anda lihat di bawah:

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

Komentari baris-baris itu di mana pun disebutkan org.h2.Driver dalam file , jika Anda tidak menggunakan database itu. Kalau tidak, Anda harus mengunduh file jar basis data itu.

Terima kasih.


0

Kesalahan ini terjadi pada saya di Aplikasi Grails dengan JTDS Driver 1.3.0 (SQL Server). Masalahnya adalah login yang salah di SQL Server. Setelah menyelesaikan masalah ini (dalam SQL Server) aplikasi saya dikerahkan dengan benar di Tomcat. Tip: Saya melihat kesalahan di stacktrace.log

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.