解决tomcat 执行shutdown.sh 未能正常中止服务,释放资源 出现如 * create a memory leak

Made with Remarkable!java

解决tomcat 执行shutdown.sh 未能正常中止服务,释放资源 出现如 * create a memory leak

近日,同事反馈说在搭建jenkins 部署,在打包完成,执行自动部署时,执行server tomcat 的shutdown.sh 后,tomcat 进程未关闭,资源未获得释放,日志输出以下:git

警告: The web application [XXXX] appears to have started a thread named [commons-pool-EvictionTimer] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread:
java.lang.Object.wait(Native Method)
java.util.TimerThread.mainLoop(Timer.java:552)
java.util.TimerThread.run(Timer.java:505)
十月 13, 2017 10:10:19 上午 org.apache.catalina.loader.WebappClassLoaderBase clearReferencesThreads
警告: The web application [XXXX appears to have started a thread named [Thread-6] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread:
sun.misc.Unsafe.park(Native Method)
java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
java.util.concurrent.locks.AbstractQueuedSynchronizer $ConditionObject.await(AbstractQueuedSynchronizer.java:2039) java.util.concurrent.LinkedBlockingDeque.takeFirst(LinkedBlockingDeque.java:492) java.util.concurrent.LinkedBlockingDeque.take(LinkedBlockingDeque.java:680) com.erp.cloudfi.report.util.LongTimeWorker.startWork(LongTimeWorker.java:38) com.erp.cloudfi.report.util.LongTimeWorker$ 1.run(LongTimeWorker.java:27)
java.lang.Thread.run(Thread.java:748) github

通过日志反馈,查询到 有两种状况会形成线程存留.咱们须要在spring 应用 shutdown时,destroy 相关资源并退出相关线程 web

1.LongTimeWorker(业务线程)引发的

代码以下: redis

public class LongTimeWorker {
    /**
     * 任务队列
     */
    BlockingQueue<Worker> works;
    public LongTimeWorker() {
        works = new LinkedBlockingDeque<Worker>();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                startWork();
            }
        });
        thread.setDaemon(true);
        thread.start();
    }
    public void startWork() {
        try {
            while(true) {
                Worker work = works.take();
                Worksheet sheet = work.getWorksheet();              
                work.run();
                if(sheet == null){
                    Thread.sleep(100);
                }else{
                    String id = sheet.getId();
                    existsKey.remove(id);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 
     * @param work
     */
    public void setWork(Worker work) {
        try {
            works.put(work);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

能够看到,在建立对象的时候,以demon 方式启动了一个 死循环的 程序,不包含退出条件.spring

修改方式以下,添加 退出标志,放弃建立对象的时候启动线程,增长启动线程方法,与退出线程方法.
修改后代码以下: 数据库

public class LongTimeWorker {
    /**
     * 任务队列
     */
    BlockingQueue<Worker> works;
    private boolean workstatus=false;
    private Thread workthread = null;
    public LongTimeWorker() {
        works = new LinkedBlockingDeque<Worker>();
        workthread = new Thread(new Runnable() {
            @Override
            public void run() {
                startWork();
            }
        });
        workstatus=false;
        workthread.setDaemon(true);
    }
    public void startWork() {
        workstatus = true;
        workthread.start();
    }
    public void stopWork() {
        workstatus=false;
    }
    public void work() {
        try {
            while(workstatus) {
                Worker work = works.take();
                Worksheet sheet = work.getWorksheet();              
                work.run();
                if(sheet == null){
                    Thread.sleep(100);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * @param work
     */
    public void setWork(Worker work) {
        try {
            works.put(work);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2.commons-pool-EvictionTimer 未正常释放资源引发的

实现类为 org.apache.commons.pool.impl.EvictionTimer 所属于 commons-pool 包下,class 描述以下apache

Provides a shared idle object eviction timer for all pools. This class wraps the standard java.util.Timer and keeps track of how many pools are using it. If no pools are using the timer, it is canceled. This prevents a thread being left running which, in application server environments, can lead to memory leads and/or prevent applications from shutting down or reloading cleanly.

This class has package scope to prevent its inclusion in the pool public API. The class declaration below should *not* be changed to public.

提供了一个基于timer 机制的共享空闲 对象的通用池.缓存

项目中涉及到 对象共享池的相关有 数据库链接池,JedisPool,由于 未使用spring 提供的 redis 模块 ,而是基于jedis 进行的封装.因此针对JedisPool 链接池 .(数据库链接池比较经常使用,之前未暴露此问题).通过检查工程代码,发现应用中 在应用启动时,声明了一个JedisPool,来进行业务缓存处理,但在应用退出时并未对jedispool 有任何处理.
参考 https://github.com/xetorthio/jedis/issues/936 tomcat

Pool needs to be closed when it is no longer used - Failed to stop thread named [commons-pool-EvictionTimer]
pool在不被使用的时候须要释放

guys from commons-pool you need to create a ServletContextListener and implement the contextDestroyed method. In thaUImethod you should get the reference to your JedisPool and call the close method.
须要在ServletContextListener 实现 contextDestroyed 方法 , 执行JedisPool 对象的destroy 方法,对资源进行释放.


在应用启动与关闭时,初始化与销毁相关资源,步骤以下:

a.实现ServletContextListener 接口,分别在contextInitialized 与 contextDestroyed 中实现本身的资源初始化与销毁逻辑(在 destroy 方法中 处理掉问题中未关闭的线程,与未关闭的redis pool )

public class CloudfiApplicationContextListener implements ServletContextListener {
        private ServletContext servletContext;
        private LongTimeWorker longTimeWorker;
        private MybatisRedisCache redis;

        public void setRedis(MybatisRedisCache redis) {
            this.redis = redis;
        }

        public void setLongTimeWorker(LongTimeWorker longTimeWorker) {
            this.longTimeWorker = longTimeWorker;
        }

        @Override
        public void contextInitialized(ServletContextEvent sce) {
            servletContext = sce.getServletContext();
            SpringContextUtil.setApplicationContext(WebApplicationContextUtils.getWebApplicationContext(servletContext));

            longTimeWorker = SpringContextUtil.getBean( LongTimeWorker.class);
            redis = SpringContextUtil.getBean(MybatisRedisCache.class);
            longTimeWorker.startWork();
        }

        @Override
        public void contextDestroyed(ServletContextEvent sce) {
            longTimeWorker.stopWork();
            redis.getJedisPool().close();
        }
    }

b.在web.xml 中配置 a 中定义的listener

<servlet>
    <servlet-name>springmvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:springmvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<listener>  
    <listener-class>org.springframework.web.context.ContextLoaderListener  
    </listener-class>  
</listener>
<listener>
    <listener-class>
        javacommon.init.CloudfiApplicationContextListener
    </listener-class>
</listener>

注意,若是须要在实现的listener 中,使用spring 中的bean ,因为listener 与 servlet 加载顺序的限制,此处只能读取 ContextLoaderListener
扫描到spring 的bean,没法获取到 DispatcherServlet 扫描到spring 的bean (若须要获取 ContextLoaderListener 的bean ,注意自定义listener 的配置须要在 ContextLoaderListener 配置以后)

至此,引发tomcat 执行 shutdown.sh 没法正常关闭的问题解决.

若是上述中出现错误,欢迎指正,如有问题请联系smn007@163.com crazy~smn 星期五, 13. 十月 2017 02:55下午

相关文章
相关标签/搜索