前奏: 最近在Linux部署tomcat应用的时候发现停止tomcat后,自己的进程还在,必须要用kill命令强行杀掉进程,每次这样做感觉很不妥,所以现在我来找一下出现这个问题的根本原因并给出解决方案。
背景:有一天我发现公司的Ubantu服务器内存不够用了……,32G内存!而且这台服务器只部署了我一个服务。what?神马情况,一脸萌币。让我静静…………。
好了,安静了,现在开始分析原因。
首先我执行了 ps|aux grep tomat(意思就是查找所有带有tomcat相关的进程),发现有N个tomcat进程在运行(截图省略),每个进程占用1个G内存左右。关键还调用不到它……,很忧桑。仔细想了想难道是以前的tomcat都没关闭成功? ./shutdown这个方法失灵了?
是的,的确失灵了,现在我们来分析一下它失灵的原因。
第一步.首先打开tomcat的logs里面catalina.out查看日志,发现出现如下日志
第二步.于是我根据它给的代码提示,找到了分词工具中的源码代码块。
从这里我们能看到它的服务里面有一个资源监控的方法,这方法运行在ExceutorService线程池里面,它的作用是为了监听目录下资源的变化,如果有变动,则重新加载资源,和web.xml配置log4j的配置文件变化监听很像。而且它使用的是while(true)死循环并且没有任何判断break操作的地方,同时根据WatchService.take我们发现它在这里进行了线程等待,每当资源变化的时候,WatchService就能take到值并返回,从而进行下一次等待。如果它运行在一个守护线程里面,当主线程关闭的时候,它会进行自动销毁。但是它是线程池开辟出来的,为非守护线程。所以如果要结束这个线程必须调用ExceutorService的shutdown方法,但是这个方法执行的前提是等待线程池里面所有的线程都完成自己的任务才会调用,很明显在我们这个死循环的代码块里面它的任务永远都不会完成。所以我们需要强制结束,调用它的shotdownNow方法。可能在它的第三方工具里面它的线程池对象是私有的,并且没有给出强制结束的方法,所以只好另辟蹊径了。
第三步.我们利用jstack堆栈跟踪工具追踪验证一下我刚刚的分析。
在linux服务器中查看我的web服务的java进程的pid,比如是123,然后输入 jstack 123。
到这里,我们已经找到了关闭失败的原因了,现在我们来看一下如何解决这个问题。
刚刚提到分词工具里面的线程池对象已经被设为私有,并且并没有提供较好的关闭方法。所以我这里找了两个方案来结束这个进程。
方案一
在程序shutdown前,设置监听,停止之前我们需要做一些事情,就是关闭这些进程。我们可以获取到所有的jvm进程并执行中断操作。
a.a.获取jvm的所有进程。
public static Thread[] findAllThreads() {
ThreadGroup group =
Thread.currentThread().getThreadGroup();
ThreadGroup topGroup = group;
// traverse the ThreadGroup tree to the top
while ( group != null ) {
topGroup = group;
group = group.getParent();
}
// Create a destination array that is about
// twice as big as needed to be very confident
// that none are clipped.
int estimatedSize = topGroup.activeCount() * 2;
Thread[] slackList = new Thread[estimatedSize];
// Load the thread references into the oversized
// array. The actual number of threads loaded
// is returned.
int actualSize = topGroup.enumerate(slackList);
// copy into a list that is the exact size
Thread[] list = new Thread[actualSize];
System.arraycopy(slackList, 0, list, 0, actualSize);
return list;
}
a.b对每个线程进行中断操作。
Thread[] threads = findAllThreads();
for(Thread thread : threads){
thread.interrupt();
}
至此,在执行tomcat ./shutdown后就可以顺利地关闭进程。
方案二
还有个更暴力的方法结束进程,就是在结束的时候执行
System.exit(0);
可以直接将进程结束,这里你可能会担心,这个方法是结束jvm虚拟机,调用这个方法会不会把我linux里面其它的java进程全部结束,这里你的担心是多余的,如果你对jvm的运行机制有一定的了解,就可以大不必担心这个了。