tomcat关闭应用时的清理工作

tomcat关闭应用时的清理工作(1): JDBC Driver

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 的 BasicDataSourceclose方法里执行反注册驱动的行为来解决这个警告。但DBCP的开发者认为这个应该是使用者的责任,不愿意接受这种建议,参考这里

tomcat关闭应用时的清理工作(2): 线程的清理

tomcat在关闭时,有时会看到类似下面的警告信息:

2014-7-10 13:44:02 org.apache.catalina.loader.WebappClassLoader clearReferencesThreads
SEVERE: The web application [] appears to have started a thread named 
[com.taobao.xxx.client.Timer] but has failed to stop it. 
This is very likely to create a memory leak.

这是tomcat关闭应用时检测到了应用启动的线程未被终止,tomcat为防止造成内存泄露,给出上面的警告,并根据配置来决定是否强制停止该线程(默认不会强制停止)。

有时也会有另一种相似的警告信息:

2014-7-10 13:44:02 org.apache.catalina.loader.WebappClassLoader clearReferencesThreads
SEVERE: The web application [] is still processing a request that has yet to finish. 
This is very likely to create a memory leak. 
You can control the time allowed for requests to finish 
by using the unloadDelay attribute of the standard Context implementation.

这是tomcat关闭应用时检测到了仍有请求线程未处理完。

上面的2种警告都是在WebappClassLoaderclearReferencesThreads方法里给出的,该方法也是在stop时调用clearReferences方法时调用的:

protected void clearReferences() {
    ...
    // Stop any threads the web application started
    clearReferencesThreads();
    ...
}

clearReferencesThreads方法里,通过找到最顶层的root thread group获取所有的active线程,然后判断这些线程如果是用户线程的话,给出警告:

if (isRequestThread(thread)) {
    log.error(sm.getString("webappClassLoader.warnRequestThread",
                            contextName, thread.getName()));
} else {
    log.error(sm.getString("webappClassLoader.warnThread",
                            contextName, thread.getName()));
}

我们来模拟一下,先看第一种情况:

@WebServlet(name = "MainServlet", urlPatterns = { "/main" }, loadOnStartup = 1)
public class MainServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    public void init() {
        new Thread() {
            public void run() {
              try {
                while (true) {
                    Thread.sleep(1000);
                }
              } catch (Exception e) {
              }
            }
        }.start();
    }
}

在一个servlet初始化时启动了一个线程,没有提供销毁这个线程的机制,当tomcat停止时,会报第一种警告。

再模拟第二种警告情况,在请求时将线程hang住:

public void doGet(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
    try {
        Thread.sleep(1000 * 200);
    } catch (Exception e) {
    }
}

这时关闭tomcat时会报出第二种警告信息。

默认tomcat并不会强制将这些线程终止,除非设置了clearReferencesStopThreads为true,它判断线程属于某个线程池则延迟一段时间将线程终止,否则直接调用了JDK已不鼓励的Thread.stop方法终止线程。

if (usingExecutor) {
    // Executor may take a short time to stop all the
    // threads. Make a note of threads that should be
    // stopped and check them at the end of the method.
    executorThreadsToStop.add(thread);
} else {
    // This method is deprecated and for good reason. This
    // is very risky code but is the only option at this
    // point. A *very* good reason for apps to do this
    // clean-up themselves.
    thread.stop();
}

tomcat关闭应用时的清理工作(3): ThreadLocal

tomcat在关闭时,可能看到与ThreadLocal相关的警告:

Jan 24, 2014 7:18:52 AM org.apache.catalina.loader.WebappClassLoader checkThreadLocalMapForLeaks
SEVERE: The web application [] created a ThreadLocal with key of type 
[java.lang.ThreadLocal] (value [java.lang.ThreadLocal@4e61bc49]) and a value of type 
[com.alibaba.xxx.Entry] (value [...]) but failed to remove it when the web application was stopped. 
Threads are going to be renewed over time to try and avoid a probable memory leak.

用下面的例子来模拟一下

@WebServlet(name = "MainServlet", urlPatterns = { "/main" }, loadOnStartup = 1)
public class MainServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    private static ThreadLocal<MyClass> tl = new ThreadLocal<MyClass>();

    public void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        MyClass m = tl.get();
        if (m == null) {
            tl.set(new MyClass());
        }
    }

}

class MyClass {}

请求一次之后,通过脚本会命令停止tomcat,会看到类似的日志:

七月 16, 2014 5:01:35 下午 org.apache.catalina.loader.WebappClassLoader checkThreadLocalMapForLeaks
严重: The web application [] created a ThreadLocal with key of type [java.lang.ThreadLocal] 
(value [java.lang.ThreadLocal@7da150]) and a value of type [org.r113.servlet3.MyClass] 
(value [org.r113.servlet3.MyClass@37e98b70]) but failed to remove it when the web application was stopped. 
Threads are going to be renewed over time to try and avoid a probable memory leak.

这个泄露其实是可能造成classloader的泄露,因为ThreadLocal引用了自定义的类MyClass,绑定到了当前的请求线程上,而请求线程又是线程池里的线程,生存周期可能会比较长。比如上面模拟的情况,要停止应用的时候,请求线程的ThreadLocal仍未释放,那么即使加载MyClass类的classLoader已经不会再被任何地方使用,可以被垃圾回收了,却因为这个MyClass被引用而得不到回收。





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值