一、问题由来

我们组用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配置:

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

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

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

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

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

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

四、问题出现的原因

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

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

  1. 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:

  1. protected void reload(PrintWriter writer, ContextName cn,
  2. StringManager smClient) {
  3.  
  4. try {
  5. Context context = (Context) host.findChild(cn.getName());
  6. 。。。。。。删除无关代码
  7. context.reload();
  8. }
  9.  
  10. }

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

  1. public synchronized void reload() {
  2.  
  3. setPaused(true);
  4.  
  5. try {
  6. stop();
  7. } catch (LifecycleException e) {
  8. }
  9. 。。。删除无关代码
  10. try {
  11. start();
  12. } catch (LifecycleException e) {
  13. }
  14.  
  15. setPaused(false);
  16.  
  17. if(log.isInfoEnabled())
  18. log.info(sm.getString("standardContext.reloadingCompleted",
  19. getName()));
  20.  
  21. }

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

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

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

  1. protected synchronized void stopInternal() {
  2.  
  3. try {
  4. // Stop our child containers, if any
  5. final Container[] children = findChildren();
  6.  
  7. for (int i = 0; i < children.length; i++) {
  8. children[i].stop();
  9. }
  10. }

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

然后调用这些servlet wrapper的stop。

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

  1. protected synchronized void stopInternal() throws LifecycleException {
  2. // Shut down our servlet instance (if it has been initialized)
  3. try {
  4. unload();
  5. } catch (ServletException e) {
  6. getServletContext().log(sm.getString
  7. ("standardWrapper.unloadException", getName()), e);
  8. }
  9.  
  10. }

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

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

  1.  
  1. protected volatile Servlet instance = null;
  1. public synchronized void unload() throws ServletException {
  2.  
  3. // Nothing to do if we have never loaded the instance
  4. if (!singleThreadModel && (instance == null))
  5. return;
  6. unloading = true;
  7.  
  8. // Call the servlet destroy() method
  9. try {
  10. instance.destroy();
  11. }
  12.  
  13. // Deregister the destroyed instance
  14. instance = null;
  15. instanceInitialized = false;
  16.  
  17. }

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

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

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

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

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

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

  1. protected void doClose() {
  2. if (this.active.get() && this.closed.compareAndSet(false, true)) {
  3.  
  4. LiveBeansView.unregisterApplicationContext(this);
  5.  
  6. try {
  7. // Publish shutdown event.
  8. publishEvent(new ContextClosedEvent(this));
  9. }
  10. catch (Throwable ex) {
  11. logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
  12. }
  13.  
  14. // Stop all Lifecycle beans, to avoid delays during individual destruction.
  15. getLifecycleProcessor().onClose();// Destroy all cached singletons in the context's BeanFactory.
  16. destroyBeans();
  17.  
  18. // Close the state of this context itself.
  19. closeBeanFactory();
  20.  
  21. // Let subclasses do some final clean-up if they wish...
  22. onClose();
  23.  
  24. this.active.set(false);
  25. }
  26. }

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

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

五、解决办法

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

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

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

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

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

2019-02-11日更新:

针对上面的第二种方法,调用线程池的shutdownNow,会循环给池里的线程调用该线程的interrupt方法。

interrupt方法,是否有效果,这个只能取决于具体的线程的run方法实现。

比如看下面我们当时线程的实现就是有问题的:

查看blockingqueue的take方法:

但我们的线程实现里,捕获了异常,继续无限循环。。。(这个是历史代码。。。哎)

所以,正确的做法是,要保证线程在被interrupt后,可以正常结束。

处理方式有几种:

参考https://www.ibm.com/developerworks/cn/java/j-jtp05236.html

1、不捕捉 InterruptedException,将它传播给调用者

2、捕获后重新抛出

3、在runable中,无法抛出时,捕获后,重新设置中断,让调用方可以感知

4、最不建议的方式:吞了异常;或者只打个日志。

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

持续集成环境--Tomcat热部署导致线程泄漏的更多相关文章

  1. JavaWeb+SVN+Maven+Tomcat +jenkins搭建持续集成环境和自动部署

    https://blog.csdn.net/wh52788/article/details/80900477 https://blog.csdn.net/liyong1028826685/articl ...

  2. [Docker][ansible-playbook]3 持续集成环境之分布式部署

    预计阅读时间: 30分钟 本期解决痛点如下:1. 代码版本的多样性,编译环境的多样性如何解决?答案是使用docker,将不同的编译环境images统统打包到私有仓库上,根据需求进行下载,从宿主机上挂载 ...

  3. [ansible-playbook]4 持续集成环境之分布式部署利器 ansible playbook学习

    3 ansible-play讲的中太少了,今天稍微深入学习一点 预计阅读时间:15分钟 一: 安装部署 参考 http://getansible.com/begin/an_zhuang_ansile ...

  4. Tomcat热部署,Web工程中线程没有终止

    近期项目中,用 jenkins 热部署 web工程时,发现工程中静态持有的线程(将ScheduledExecutorService定时任务存储在静态Map中),导致不定时出现数据库访问事务关闭异常,如 ...

  5. 使用Maven+Nexus+Jenkins+Svn+Tomcat+Sonar搭建持续集成环境(一)

    前言     但凡一个略有规模的项目都需要一个持续集成环境的支撑,为什么需要持续集成环境,我们来看一个例子.假如一个项目,由A.B两位程序员来协作开发,A负责前端模块,B负责后端模块,前端依赖后端.A ...

  6. 使用Maven+Nexus+Jenkins+Svn+Tomcat+Sonar搭建持续集成环境

    前言 但凡一个略有规模的项目都需要一个持续集成环境的支撑,为什么需要持续集成环境,我们来看一个例子.假如一个项目,由A.B两位程序员来协作开发,A负责前端模块,B负责后端模块,前端依赖后端.A和B都习 ...

  7. Maven+Nexus+Jenkins+Svn+Tomcat+Sonar搭建持续集成环境

    使用Maven+Nexus+Jenkins+Svn+Tomcat+Sonar搭建持续集成环境(一) 2015-01-14 20:28 by 飘扬的红领巾, 4322 阅读, 5 评论, 收藏, 编辑 ...

  8. 使用Maven+Nexus+Jenkins+Svn+Tomcat+Sonar搭建持续集成环境(二)

    前言     上一篇随笔Maven+Nexus+Jenkins+Svn+Tomcat+Sonar搭建持续集成环境(一)介绍maven和nexus的环境搭建,以及如何使用maven和nexus统一管理库 ...

  9. Maven+Nexus+Jenkins+Svn+Tomcat+Sonar搭建持续集成环境(二)

    上一篇随笔Maven+Nexus+Jenkins+Svn+Tomcat+Sonar搭建持续集成环境(一)介绍maven和nexus的环境搭建,以及如何使用maven和nexus统一管理库文件和版本,以 ...

随机推荐

  1. idea出现插件突然失灵解决方案

    File -> Settings  -> Plgins  把失效的插件重新去掉打钩并重新打钩即可

  2. Java多线程——线程范围内共享变量和ThreadLocal

    多个线程访问共享对象和数据的方式 1.如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据,例如,买票系统就可以这么做. package java_ ...

  3. iOS : Blur Effect

    http://blog.bubbly.net/2013/09/11/slick-tricks-for-ios-blur-effect/ Full sample sources available at ...

  4. UpdateData()用法

    一.总结UpdateData()函数 UpdateData(true);//用于将屏幕上控件中的数据交换到变量中. UpdateData(false);//用于将数据在屏幕中对应控件中显示出来.    ...

  5. kafka学习之-集群配置及安装

    1.软件版本 kafka2.10_0.9.0.0 zookeeper_3.4.6 2.集群节点 一共有3台机器. 192.168.14.100 slave-01 192.168.14.105 slav ...

  6. 【NLP】Stanford

    http://web.stanford.edu/class/cs224n/syllabus.html https://www.youtube.com/watch?v=OQQ-W_63UgQ&l ...

  7. 使用jstl+el表达式遇到的几个问题

    1.使用jstl访问Map<Integer,String>中的内容时总取不到? el表达式的一个bug,在解析数字的时候,会自动将数字转换成Long类型. 我的解决办法是,Map的key改 ...

  8. linux环境中,ssh登录报错,Permission denied, please try again.

    问题描述: 今天早上一个同事反应一个问题,通过ssh登录一台测试机的时候,发现两个账号,都是普通账号,一个账号能够登录, 另外一个账号无法登录.问他之前有做过什么变更吗,提到的就是之前有升级过open ...

  9. phoenix客户端连接hbase数据库报错:Traceback (most recent call last): File "bin/sqlline.py", line 27, in <module> import argparse ImportError: No module named argparse

    环境描述: 操作系统版本:CentOS release 6.5 (Final) phoenix版本:phoenix-4.10.0 hbase版本:hbase-1.2.6 现象描述: 通过phoenix ...

  10. event.keyCode与event.which

        //Netscape/Firefox/Opera中不支持 window.event.keyCode,需要用event.which代替//IE用event.keCode方法获取当前被按下的键盘按 ...