tomcat在关闭应用时,对资源做了一些清理,避免了泄露,这个工作主要是WebappClassLoader
里做的,WebappClassLoader
也实现自Lifecycle
接口,在应用关闭时,会触发其stop
方法:
@Override
public void stop() throws LifecycleException {
// Clearing references should be done before setting started to
// false, due to possible side effects
clearReferences();
......
}
释放引用的工作主要在clearReferences
里:
protected void clearReferences() {
// De-register any remaining JDBC drivers
clearReferencesJdbc();
// Stop any threads the web application started
clearReferencesThreads();
// Check for leaks triggered by ThreadLocals loaded by this class loader
checkThreadLocalsForLeaks();
// Clear RMI Targets loaded by this class loader
clearReferencesRmiTargets();
// Null out any static or final fields from loaded classes,
// as a workaround for apparent garbage collection bugs
if (clearReferencesStatic) {
clearReferencesStaticFinal();
}
// Clear the IntrospectionUtils cache.
IntrospectionUtils.clear();
// Clear the classloader reference in common-logging
if (clearReferencesLogFactoryRelease) {
org.apache.juli.logging.LogFactory.release(this);
}
// Clear the resource bundle cache
// This shouldn't be necessary, the cache uses weak references but
// it has caused leaks. Oddly, using the leak detection code in
// standard host allows the class loader to be GC'd. This has been seen
// on Sun but not IBM JREs. Maybe a bug in Sun's GC impl?
clearReferencesResourceBundles();
// Clear the classloader reference in the VM's bean introspector
java.beans.Introspector.flushCaches();
}
其中对JDBC Driver的清理,是clearReferencesJdbc
方法,它检查当前WebappClassLoader加载过的,在关闭时未注销掉的JDBC Driver,给出警告信息,并强行将这些Driver反注册掉。所以我们有时在关闭时会看到类似下面的信息:
SEVERE: The web application [xxx] registered the JDBC driver
[com.mysql.jdbc.Driver] but failed to unregister it when the web application was stopped.
To prevent a memory leak, the JDBC Driver has been forcibly unregistered.
我们通过下面的例子来模拟一下这个现象:
@WebServlet(name = "MainServlet", urlPatterns = { "/main" }, loadOnStartup=1)
public class MainServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public void init() {
try {
Class.forName("com.alibaba.druid.mock.MockDriver");
} catch (Exception e) {
e.printStackTrace();
}
}
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
PrintWriter wr = resp.getWriter();
wr.write("ok");
wr.flush();
}
}
上面的servlet在初始化时注册了一个Driver,但销毁时未将这个Driver给反注册掉;这时不管是显式的通过命令来stop tomcat,还是因为设置了自动reload,而且恰好检查到应用有变,执行了reload的时候(reload也是对app context进行stop,然后再重新start),就会被tomcat判断为泄露,给出警告并强制反注册Driver:
$ kill `pidof java`
// 在日志里会看到
严重: The web application [] registered the JDBC driver [com.alibaba.druid.mock.MockDriver]
but failed to unregister it when the web application was stopped.
To prevent a memory leak, the JDBC Driver has been forcibly unregistered.
要避免这个信息,应用或框架应该自己来保证在销毁时将JDBC Driver反注册掉。例如在destroy方法里:
@Override
public void destroy() {
super.destroy();
try{
DriverManager.deregisterDriver(DriverManager.getDrivers().nextElement());
}catch(Exception e){
e.printStackTrace();
}
}
因为tomcat自带了DBCP数据库连接池,很多用户在使用DBCP时遇到了这个问题,并建议在 DBCP 的 BasicDataSource
的close
方法里执行反注册驱动的行为来解决这个警告。但DBCP的开发者认为这个应该是使用者的责任,不愿意接受这种建议,参考这里。