为了账号安全,请及时绑定邮箱和手机立即绑定

持续集成环境--Tomcat热部署导致线程泄漏

标签:
Java

一、问题由来

我们组用jenkins部署了持续集成环境,(jenkins部署war包到远程服务器的tomcat)。

每次提交了代码,jenkins上一键构建,就可以自动拉取最新代码,打war包,热部署到远程环境上的tomcat。

一切都很好,只是一次用jconsole偶然连上去一看,远程环境上的tomcat上,线程数竟多达700多个。。。

 

回到顶部

二、排查代码

查看线程堆栈,几百个线程中,线程名为“UserService-InformImAndCcm”打头的,多达130+,但是在代码中,只搜到一处线程池配置:

 

一个qq群里,有人说我们的参数配错了,我一度动摇了,但后来还是觉得不对,我理解的线程池就是:

超过核心线程数后,仍然有task,就丢队列,如果队列满了,就继续开线程,直到达到maximumPoolSize,如果后续队列再满了,则拒绝任务。

也就是说,线程不可能超过maximumPoolSize。

。。。

后来任务一多,忘了。今天又想起来,做个测试,因为我感觉,这事,可能和热部署有关系。

 

回到顶部

三、本地测试--多次热部署同一应用

1、本地环境配置

很简单,一个war包,两个tomcat自带的war包,用来控制reload应用。

 

配置好了后,启动tomcat

 

2、打开jconsole进行监控

主要是监控线程。

 

3、reload应用一次

打开localhost:9080/manager/html,如果不能访问,请在tomcat下面的conf中的tomcat-users.xml配置:

    <role rolename="manager-gui"/>
    <user username="admin" password="admin" roles="manager-gui"/>

 

 

 4、观察jconsole中的线程数是否增加

 5、反复重试前面3-4步

如果不出意外(程序中有线程泄漏)的话,jconsole中的线程图应该是下面这样,一步一个台阶:

 

 6、查看tomcat下logs中的catalina.log

这里面可能会有些线程泄漏的警告,如下:

 

回到顶部

四、问题出现的原因

Tomcat热部署的实现机制,暂时没有研究。

不过根据在catalina.log日志中出现的:

26-Dec-2018 13:06:24.920 信息 [http-nio-9081-exec-34] org.apache.catalina.core.StandardContext.reload Reloading Context with name [/CAD_WebService] is completed

在idea中通过如下骚操作:

找到了关联的源码:

 

进入该Servlet的reload:

复制代码

protected void reload(PrintWriter writer, ContextName cn,
            StringManager smClient) {        try {
            Context context = (Context) host.findChild(cn.getName());
            。。。。。。删除无关代码            context.reload();        }

    }

复制代码

 

这里的context,实现类是org.apache.catalina.core.StandardContext,该类的reload方法:

复制代码

public synchronized void reload() {

        setPaused(true);        try {            stop();
        } catch (LifecycleException e) {
        }
       。。。删除无关代码        try {            start();
        } catch (LifecycleException e) {
        }

        setPaused(false);        if(log.isInfoEnabled())
            log.info(sm.getString("standardContext.reloadingCompleted",
                    getName()));

    }

复制代码

 

StandardContext类,未实现自己的stop,因此调用了基类org.apache.catalina.util.LifecycleBase#stop:

public final synchronized void stop() throws LifecycleException {            stopInternal();  //无关代码已删除
 }

在org.apache.catalina.core.StandardContext中,重写了stopInternal:

复制代码

    protected synchronized void stopInternal()  {        try {            // Stop our child containers, if any
            final Container[] children = findChildren();            for (int i = 0; i < children.length; i++) {                children[i].stop();
            }
    }

复制代码

在这里,会查找当前对象(当前对象代表我们要reload的context,即一个应用),这里查找它下面的子container,那就是会查找到各servlet的wrapper。

然后调用这些servlet wrapper的stop。

wrapper的标准实现为:org.apache.catalina.core.StandardWrapper。其stopInternal如下:

复制代码

protected synchronized void stopInternal() throws LifecycleException {        // Shut down our servlet instance (if it has been initialized)
        try {            unload();
        } catch (ServletException e) {
            getServletContext().log(sm.getString
                      ("standardWrapper.unloadException", getName()), e);
        }

    }

复制代码

这里准备在unload中,关闭servlet。

org.apache.catalina.core.StandardWrapper#unload:

复制代码


protected volatile Servlet instance = null;
public synchronized void unload() throws ServletException {        // Nothing to do if we have never loaded the instance
        if (!singleThreadModel && (instance == null))            return;
        unloading = true;            // Call the servlet destroy() method
            try {                    instance.destroy();             }        // Deregister the destroyed instance
        instance = null;
        instanceInitialized = false;

    }

复制代码

从上看出,这里开始调用servlet的destroy方法了。

 

spring应用的servlet,想必大家都很熟了,org.springframework.web.servlet.DispatcherServlet。

它的destroy方法由父类org.springframework.web.servlet.FrameworkServlet实现,#destroy:

复制代码

    public void destroy() {
        getServletContext().log("Destroying Spring FrameworkServlet '" + getServletName() + "'");        // Only call close() on WebApplicationContext if locally managed...
        if (this.webApplicationContext instanceof ConfigurableApplicationContext && !this.webApplicationContextInjected) {
            ((ConfigurableApplicationContext) this.webApplicationContext).close();
        }
    }

复制代码

 

这里,主要是针对spring 容器进行关闭,比如各种bean的close方法等等。

实现在这里,org.springframework.context.support.AbstractApplicationContext#doClose:

复制代码

protected void doClose() {        if (this.active.get() && this.closed.compareAndSet(false, true)) {

            LiveBeansView.unregisterApplicationContext(this);            try {                // Publish shutdown event.
                publishEvent(new ContextClosedEvent(this));
            }            catch (Throwable ex) {
                logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
            }            // Stop all Lifecycle beans, to avoid delays during individual destruction.
            getLifecycleProcessor().onClose();// Destroy all cached singletons in the context's BeanFactory.            destroyBeans();            // Close the state of this context itself.            closeBeanFactory();            // Let subclasses do some final clean-up if they wish...            onClose();            this.active.set(false);
        }
    }

复制代码

 

问题分析到现在,我们可以发现,针对spring bean中的线程池,是没有地方去关闭线程池的。

所以,每次reload,在stop的过程中,线程池都没得到关闭,于是造成了线程泄漏。

 

回到顶部

五、解决办法

1:网上的解决办法是说:实现一个javax.servlet.ServletContextListener,实现其jcontextDestroyed方法,然后注册到servlet中。

 

2:我这边觉得,按照上面的分析,直接在关闭bean的时候,关闭线程池也可以:

针对,spring应用,在bean中,如果有线程池实例变量的话,让bean实现org.springframework.beans.factory.DisposableBean接口:

    @Override    public void destroy() throws Exception {
        logger.info("about to shutdown thread pool");
        pool.shutdownNow();
    }

 

 不过说实话,上面的两种方案我都试了,不起作用。明天弄个纯净的工程试下吧,目前的project里代码太杂。

 如果大家有什么想法,欢迎和我交流

 

原文出处:https://www.cnblogs.com/grey-wolf/p/10179895.html  

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消