問題定位及排查
上周無意中調試程式在Linux上ps -ef|grep tomcat發現有許多tomcat的程序,當時因為沒有影響系統運作就沒當回事。而且我内心總覺得這可能是tomcat像nginx一樣啟動多個程序。
後來測試在一次更新後回報說怎麼現在tomcat程序無法shutdown?這讓我有點意外,看來這個問題并沒有這麼簡單。于是開始思考問題會出在哪裡。
複現問題
先是另外一台伺服器部署,然後shutdown後再ps程序是空的,這說明tomcat不會自動産生新的程序。那就有可能系統代碼出了什麼問題吧?最近另一個位同僚有比較多的修改,可能是因為這些修改吧。光猜想也找不到問題,隻好用jvisuale來看一下系統的dump,發現shutdown之後程序沒有退出,而且裡面有許多線程還在運作,有些還是線程池。
看來是有線程沒有釋放導緻的洩露吧?于是用tail指令打開catalina.out檢視最後shutdown.sh,在控制台輸出了下面這些内容:
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消息。這樣做的好處是開啟線程數少,效率高,并發性能很不錯。
檢視代碼
先看一下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();
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伺服器發來的資料流。檢視線程構造函數:
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釋放資源即可。
注:此文章為原創,歡迎轉載,請在文章頁面明顯位置給出此文連結!
若您覺得這篇文章還不錯請點選下右下角的推薦,非常感謝!