上周无心中调试程序在Linux上ps -ef|grep tomcat发现有许多tomcat的进程,当时由于没有影响系统运行就没当回事。并且我心里总以为这多是tomcat像nginx同样启动多个进程。nginx
后来测试在一次升级后反馈说怎么如今tomcat进程没法shutdown?这让我有点意外,看来这个问题并无这么简单。因而开始思考问题会出在哪里。web
先是另一台服务器部署,而后shutdown后再ps进程是空的,这说明tomcat不会自动产生新的进程。那就有可能系统代码出了什么问题吧?最近另外一个位同事有比较多的修改,多是由于这些修改吧。光猜测也找不到问题,只好用jvisuale来看一下系统的dump,发现shutdown以后进程没有退出,并且里面有许多线程还在运行,有些仍是线程池。spring
看来是有线程没有释放致使的泄露吧?因而用tail命令打开catalina.out查看最后shutdown.sh,在控制台输出了下面这些内容:apache
Nov 28, 2016 10:41:08 AM org.apache.catalina.loader.WebappClassLoader clearReferencesThreads SEVERE: The web application [/] appears to have started a thread named [Component socket reader] but has failed to stop it. This is very likely to create a memory leak.
确实有许多的线程没有关闭,在关闭时还提示了泄漏。从这些线程的名字能够确认了,是这近新增了一个openfire的whack外部组件致使的。这个whack能够链接到openfire服务器,实现一套扩展组件服务的功能,咱们主要用来发送IM消息。这样作的好处是开启线程数少,效率高,并发性能很不错。tomcat
先看一下ExternalComponentManager的实现,由于它是用来外部扩展组件的管理者,咱们的操做基本是根据它来完成的。服务器
下面的代码即是是建立一个ExternalComponentManager,而且设置参数同时链接到服务器。多线程
private void CreateMessageSender() { manager = new ExternalComponentManager(configHelper.getOpenfireHost(), configHelper.getOpenfireExternalCompPort()); manager.setSecretKey(SENDER_NAME, configHelper.getOpenfirePwd()); manager.setMultipleAllowed(SENDER_NAME, true); try { msc = new MessageSenderComponent("senderComponent", manager.getServerName()); manager.addComponent(SENDER_NAME, msc); } catch (ComponentException e) { logger.error("CreateMessageSender error.", e); } }
那么最重要的是在哪里启动了线程?毕竟最终影响系统的是线程没有关闭。因此沿着addComponent这调用看看吧:并发
public void addComponent(String subdomain, Component component, Integer port) throws ComponentException { if (componentsByDomain.containsKey(subdomain)) { if (componentsByDomain.get(subdomain).getComponent() == component) { // Do nothing since the component has already been registered return; } else { throw new IllegalArgumentException("Subdomain already in use by another component"); } } // Create a wrapping ExternalComponent on the component ExternalComponent externalComponent = new ExternalComponent(component, this); try { // Register the new component componentsByDomain.put(subdomain, externalComponent); components.put(component, externalComponent); // Ask the ExternalComponent to connect with the remote server externalComponent.connect(host, port, subdomain); // Initialize the component JID componentJID = new JID(null, externalComponent.getDomain(), null); externalComponent.initialize(componentJID, this); } catch (ComponentException e) { // Unregister the new component componentsByDomain.remove(subdomain); components.remove(component); // Re-throw the exception throw e; } // Ask the external component to start processing incoming packets externalComponent.start(); }
代码也比较简单,就是建立了一个wapper类ExternalComponent将咱们本身的Component包装了一下。其中最为重要的是最后一句:externalComponent.start();app
public void start() { // Everything went fine so start reading packets from the server readerThread = new SocketReadThread(this, reader); readerThread.setDaemon(true); readerThread.start(); // Notify the component that it will be notified of new received packets component.start(); }
原来这里启动了一个读取线程,用于接收Openfire服务器发来的数据流。查看线程构造函数:dom
public SocketReadThread(ExternalComponent component, XPPPacketReader reader) { super("Component socket reader"); this.component = component; this.reader = reader; }
能够看到,这个线程的名字是“Component socket reader”,在前面的日志里确实有这个线程。
那么接下来的主要问题是如何关闭这个SocketReadThread,按理说会有相应的实现,发现externalComponent.start()这个方法有名字叫star,那么是否是有与其匹配的方法呢?确实有的一个shutdown的方法:
public void shutdown() { shutdown = true; // Notify the component to shutdown component.shutdown(); disconnect(); }
原来这里调用了component.shutdown();最后还调用了一个disconnect,继续看代码:
private void disconnect() { if (readerThread != null) { readerThread.shutdown(); } threadPool.shutdown(); TaskEngine.getInstance().cancelScheduledTask(keepAliveTask); TaskEngine.getInstance().cancelScheduledTask(timeoutTask); if (socket != null && !socket.isClosed()) { try { synchronized (writer) { try { writer.write("</stream:stream>"); xmlSerializer.flush(); } catch (IOException e) { // Do nothing } } } catch (Exception e) { // Do nothing } try { socket.close(); } catch (Exception e) { manager.getLog().error(e); } } }
发现这里就有了线程shutdown的调用,OK,说明就是它了。
由于最外层代码使用的是ExternalComponentManager,那么在ExternalComponentManager中调用了ExternalComponent shutdown的方法是removeComponent,那么就是它了。
也就是说只要在最后应用关闭时调用removeComponent方法就能够释放线程资源。这里固然就能够借助ServletContextListener来完成咯。
public class MessageSenderServletContextListener implements ServletContextListener{ private final static Logger logger = LoggerFactory .getLogger(MessageSenderServletContextListener.class); @Override public void contextInitialized(ServletContextEvent sce) { logger.debug("contextInitialized is run."); } @Override public void contextDestroyed(ServletContextEvent sce) { logger.debug("contextDestroyed is run."); MessageSender msgSender = SpringUtil.getBean(MessageSender.class); try { msgSender.shutdown(); logger.debug("MessageSender is shutdown."); } catch (ComponentException e) { logger.error(e.getMessage()); } } }
实现contextDestroyed方法,从spring中得到MessageSender类,调用shutdown释放资源便可。