为防止内存泄漏,JDBC驱动程序已被强制取消注册

mon*_*ona 316 java tomcat jdbc

我运行我的Web应用程序时收到此消息.它运行正常,但我在关机期间收到此消息.

严重:Web应用程序注册了JBDC驱动程序[oracle.jdbc.driver.OracleDriver],但在Web应用程序停止时无法注销它.为防止内存泄漏,JDBC驱动程序已被强制取消注册.

任何帮助赞赏.

Bal*_*usC 295

从版本6.0.24开始,Tomcat附带了内存泄漏检测功能,当webapp中的JDBC 4.0兼容驱动程序在webapp启动期间使用API/WEB-INF/lib自动注册时,它会导致这种警告消息,但是在webapp关闭期间没有自动注销.这条消息纯粹是非正式的,Tomcat已经相应地采取了内存泄漏防护措施.ServiceLoader

你能做什么?

  1. 忽略这些警告.Tomcat正在做好自己的工作.实际的错误是在别人的代码(有问题的JDBC驱动程序)中,而不是在你的代码中.很高兴Tomcat正确地完成了它的工作并等到JDBC驱动程序供应商修复它以便你可以升级驱动程序.另一方面,您不应该在webapp中删除JDBC驱动程序/WEB-INF/lib,而只能在服务器中删除/lib.如果你仍然将它保存在webapp中/WEB-INF/lib,那么你应该使用a手动注册和取消注册ServletContextListener.

  2. 降级到Tomcat 6.0.23或更早版本,这样您就不会被这些警告所困扰.但它会默默地不断泄露记忆.不知道毕竟这是否有用.这些内存泄漏是Tomcat hotdeployments中出现OutOfMemoryError问题的主要原因之一.

  3. 将JDBC驱动程序移动到Tomcat的/lib文件夹,并使用连接池数据源来管理驱动程序.请注意,Tomcat的内置DBCP在关闭时不会正确注销驱动程序.另请参阅错误DBCP-322,它作为WONTFIX关闭.您更愿意将DBCP替换为另一个连接池,该连接池比DBCP更好地完成其工作.例如HikariCP,BoneCPTomcat JDBC Pool.

  • 如果选项(1)是要走的路,为什么Tomcat会将这些记录为SEVERE?对我来说严重意味着"页面管理员",而不是"忽略". (47认同)
  • 这是个好建议.它没有警告内存泄漏,这是一个警告,Tomcat采取了一些强制措施来防止泄漏 (24认同)
  • 为什么不自己动手 - 而不是期望Tomcat这样做.在我看来,清理我们凌乱的代码并不是Tomcat的工作.请参阅下面的答案. (7认同)
  • @sproketboy:嗯?您是否将JDBC工件分配为类的字段,而这些字段又存储在HTTP会话中? (2认同)
  • 我想这通常是原因3(在wAR中使用库而不是`lib`). (2认同)

ae6*_*6rt 160

在您的servlet上下文侦听器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);
            }

        }
Run Code Online (Sandbox Code Playgroud)

  • 这在共享环境中可能不安全,因为您可能不希望取消注册可用的_all_ JDBC驱动程序.请参阅[我的回答](http://stackoverflow.com/a/23912257/889583)以获得更安全的方法. (16认同)
  • 有用!http://www.javabeat.net/servletcontextlistener-example/可能有助于实现servlet上下文侦听器 (3认同)

meg*_*lop 78

虽然Tomcat强制为您注销JDBC驱动程序,但是如果您移动到另一个不执行Tomcat执行的内存泄漏防护检查的servlet容器,清除上下文销毁时由webapp创建的所有资源仍然是一种很好的做法.

但是,一揽子驾驶员注销的方法是危险的.DriverManager.getDrivers()方法返回的一些驱动程序可能已由父类ClassLoader(即servlet容器的类加载器)加载而不是webapp上下文的ClassLoader(例如,它们可能位于容器的lib文件夹中,而不是webapp的文件夹中,因此在整个容器中共享).取消注册这些将影响可能正在使用它们的任何其他Web应用程序(甚至容器本身).

因此,在取消注册之前,应检查每个驱动程序的ClassLoader是否为webapp的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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • @ user11153不,我们正在检查它是否是完全相同的ClassLoader实例,而不是它是两个值相等的独立实例. (6认同)
  • 虽然其他人似乎正确处理了有问题的问题,但是当war文件被删除然后被替换时会导致问题.在这种情况下,驱动程序被取消注册并且永远不会返回 - 只有Tomcat重启才能让你走出那个洞.这个解决方案避免了地狱. (3认同)
  • 如果您使用 H2 或 PostgreSQL,这会导致驱动程序在重新加载时不会再次注册。两个驱动程序都维护一个内部注册状态,如果驱动程序只是从“DriverManager”中取消注册,则不会清除该状态。我在 https://github.com/spring-projects/spring-boot/issues/2612#issuecomment-401264199 留下了更详细的评论 (2认同)

Spi*_*der 25

我看到这个问题出现了很多.是的,Tomcat 7会自动取消注册,但它真的可以控制你的代码和良好的编码实践吗?当然,您想知道您拥有所有正确的代码来关闭所有对象,关闭数据库连接池线程,并删除所有警告.我当然这样做.

我就是这样做的.

第1步:注册监听器

web.xml中

<listener>
    <listener-class>com.mysite.MySpecialListener</listener-class>
</listener>
Run Code Online (Sandbox Code Playgroud)

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

}
Run Code Online (Sandbox Code Playgroud)

请随时评论和/或添加...

  • 它应该是实现javax.servlet.ServletContextListener,而不是扩展ApplicationContextListener? (9认同)
  • 但是`DataSource`没有`close`方法 (4认同)
  • @matthaeus我认为第1步不是必需的,`lookup`似乎只是获得了一些你不需要的对象.第3步完全没用.它肯定没有增加任何安全性,看起来像初学者会做的事情,谁不明白GC是如何工作的.我会选择http://stackoverflow.com/a/5315467/897024并删除所有驱动程序. (4认同)
  • @kapep删除所有驱动程序是危险的,因为某些驱动程序可能在容器中共享.有关仅删除webapp的ClassLoader加载的驱动程序的方法,请参阅[我的回答](http://stackoverflow.com/a/23912257/889583). (4认同)
  • 在`contextDestroyed`方法中,这些操作的顺序是否有原因?为什么在执行步骤2之前执行步骤1.其中`initContext`,`envContext`和`datasource`根本没有被引用?我问,因为我不明白第3步. (2认同)

小智 14

这纯粹是mysql驱动程序或tomcats webapp-classloader中的驱动程序注册/注销问题.将mysql驱动程序复制到tomcats lib文件夹(因此它由jvm直接加载,而不是由tomcat加载),并且消息将消失.这使得mysql jdbc驱动程序仅在JVM关闭时被卸载,然后没有人关心内存泄漏.

  • @Florito - 你必须从你的网络应用程序WEB-INF/lib中删除它 (4认同)
  • 这不起作用...我试图复制jdbc驱动程序你说:TOMCAT_HOME/lib/postgresql-9.0-801.jdbc4.jar - Tomcat 7.0\lib\postgresql-9.0-801.jdbc4.jar没有结果... (2认同)

Tim*_*imP 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>
Run Code Online (Sandbox Code Playgroud)


And*_*tto 8

针对每个应用部署的解决方案

这是我为解决问题而编写的一个监听器:如果驱动程序已经注册并且相应地执行操作,它会自动检测

重要说明:它用于在WEB-INF/lib中部署驱动程序jar时使用,而不是像许多人建议的那样在Tomcat/lib中部署,这样每个应用程序都可以处理自己的驱动程序并在未受影响的Tomcat上运行.这就是恕我直言的方式.

只需在web.xml之前配置监听器,然后再享受.

web.xml顶部附近添加:

<listener>
    <listener-class>utils.db.OjdbcDriverRegistrationListener</listener-class>    
</listener>
Run Code Online (Sandbox Code Playgroud)

另存为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");
        }

    }

}
Run Code Online (Sandbox Code Playgroud)


Col*_*ers 6

我将在Spring论坛上添加这些内容.如果将JDBC驱动程序jar移动到tomcat lib文件夹,而不是使用webapp将其部署,则警告似乎消失了.我可以确认这对我有用

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


小智 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");
}
Run Code Online (Sandbox Code Playgroud)

  • 这在共享环境中可能不安全,因为您可能不希望取消注册可用的_all_ JDBC驱动程序.请参阅[我的回答](http://stackoverflow.com/a/23912257/889583)以获得更安全的方法.此外,这应该在ServletContextListener中完成,而不是基于每个servlet,因为您的JDBC驱动程序在您的webapp中的所有servlet之间共享. (5认同)