上週無心中調試程序在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釋放資源便可。