部署新包的时候,发现tomcat起不来了,一看内存,服务器内存吃光了,进而发现java有多余进程
然后发现 tomcat shutdown时,并没有释放java,造成内存奔溃了
参考此帖:https://zhidao.baidu.com/question/433533916441175764.html
可能存在多个tomcat进程或者是由于软件在tomcat中开启了新的线程,而且未设置成daemon,造成的主线程不能退出。
解决方案:
可以使用ps命令先查找到所有的tomcat进行,然后依次处理。
注意:以下例子仅仅作为操作操作参考
查看tomcat进程
[root@localhost bin]# ps -ef|grep tomcat
root 1513 1 2 23:41 pts/1 00:00:01 /usr/local/share/java/jdk1.6.0_25/bin/java -Djava.util.logging.config.file=/opt/apache-tomcat-6.0.32/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djava.endorsed.dirs=/opt/apache-tomcat-6.0.32/endorsed -classpath /opt/apache-tomcat-6.0.32/bin/bootstrap.jar -Dcatalina.base=/opt/apache-tomcat-6.0.32 -Dcatalina.home=/opt/apache-tomcat-6.0.32 -Djava.io.tmpdir=/opt/apache-tomcat-6.0.32/temp org.apache.catalina.startup.Bootstrap start
杀死tomcat
[root@localhost bin]# kill -9 1513
先干掉多余的java进程
然后,有这么一段:
tomcat shutdown 后经常会发现仍然有进程存在,其中tomcat/bin 目录下的catalina.sh是比较常用的shell,一般开启关闭tomcat操作如下:
1 2 3 4 | |
但是往往一个工程,开发一段时间后,会发现./catalina.sh stop关闭不了tomcat,而必须使用kill -9 <pid> 这样的强制命令去
杀死tomcat,这么做当然可以,但是手法不是那么的优雅
在tomat未被./catalina stop关闭的情况下,导致误以为tomcat已经关闭成功的哥们 会在更新完代码后,./catalina start一下,于是在服务器中就产生了2个tomcat的实例,log混乱,而后每次都用kill -9 <pid> 才放心。
其实不用那样,一般关闭不了的情况,是由于程序员自己在tomcat中开启了新的线程,而且未设置成daemon,造成的主线程不能退出.
怎么发现工程里到底哪里开启的新的非守护线程呢,其实jdk提供了jstack工具,可以帮助我们分析
查看方法很简单
$JAVA_HOME/bin/jstack <pid>
pid是指进程ID, 用ps -ef|grep tomcat 就可以查看到:
1 2 3 | |
这里看到的进程ID是 1513
调用jstack查看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | |
其中看到"Attach Listener" daemon prio=10 tid=0xb41d7c00 nid=0x606 waiting on condition [0x00000000]
java.lang.Thread.State: RUNNABLE
这行,最前变的"Attach Listener" 是线程名, 紧跟其后的 daemon是线程的守护状态,
其中主线程不是daemon的,所以是这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
在"main" 后没有daemon,看到这样的线程状态,顺藤摸瓜,找到对应new Thread的地方setDaemon(true)就可以,痛痛快快的./catalina stop了
于是想起刚用了线程池,将线程池改为直接实例化线程对象,结果显示并未造成tomcat关不掉的现象,所以将锁定在线程池上。
原来,
new Thread建立的线程都是守护线程,原因就是新建线程默认上使用建立线程时的当前线程所处的守护状态。tomcat的请求处理线程为守护线程,所以我们一般情况下建立的线程也是守护线程。然而,Executors除外。
这里我采用方案二,实现ServletContextListener接口 ,
项目是spring boot而非spring mvc,故参考此帖:
http://412887952-qq-com.iteye.com/blog/2292475
添加监听:
@WebListener
public class MyServletContextListener implements ServletContextListener {
@Override
public void contextDestroyed(ServletContextEvent arg0) {
System.out.println("ServletContex销毁");
ThreadUtil.shutDown();
}
@Override
public void contextInitialized(ServletContextEvent arg0) {
System.out.println("ServletContex初始化");
}
}
在销毁中 调用ThreadUtil的shutDown函数:
public class ThreadUtil {
private static ExecutorService service = Executors.newFixedThreadPool(100);
public static void submit(Runnable runnable) {
service.submit(runnable);
// runnable.run(); // 此举是ok的,默认守护线程
}
public static void shutDown() {
service.shutdownNow();
}
}
ok,搞定。
值得注意的是:
必须调用shutdownNow函数,shutdown与shutdownNow的区别,见:http://justsee.iteye.com/blog/999189
shutDown()
当线程池调用该方法时,线程池的状态则立刻变成SHUTDOWN状态。此时,则不能再往线程池中添加任何任务,否则将会抛出RejectedExecutionException异常。但是,此时线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。
shutdownNow()
根据JDK文档描述,大致意思是:执行该方法,线程池的状态立刻变成STOP状态,并试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,当然,它会返回那些未执行的任务。
它试图终止线程的方法是通过调用Thread.interrupt()方法来实现的,但是大家知道,这种方法的作用有限,如果线程中没有sleep 、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的。所以,ShutdownNow()并不代表线程池就一定立即就能退出,它可能必须要等待所有正在执行的任务都执行完成了才能退出。
附件1:
最近研究embeded tomcat,特别是关于tomcat启动和关闭的模块。通过查看相应的源代码, 我们知道tomcat的关闭是通过往相应的关闭端口发送指定的关闭指令来达到关闭tomcat的目的。但是有的时候,通过shutdown.bat或 shutdown.sh却不能有效地关闭tomcat,网上也有很多人提出这个问题。通过相关资料,最后问题出现线程上。
首先看java虚拟机退出的条件,如下所示:
a,调用了 Runtime 类的 exit 方法,并且安全管理器允许退出操作发生。
b,非守护线程的所有线程都已停止运行,无论是通过从对 run 方法的调用中返回,还是通过抛出一个传播到 run 方法之外的异常。
如上所示,第一条是通过exit退出,第二条指出了一个正常程序退出的条件,就是所有的非守护线程都已经停止运行。我们看相应embed tomcat的启动代码,如下所示:
- tomcat.start();
-
tomcat.getServer().await();
[java] view plain copy
- tomcat.start();
-
tomcat.getServer().await();
最后一条有效的运行命令即是await,通过调用shutdown命令时,这个await就会成功的返回。按照常理来说,整个程序即会成功的完成。但是程序有时候并没有成功的结束,原因就在于程序中还存在着非守护进程。
对于tomcat来说,tomcat程序中开启的所有进程都是守护进程,所以tomcat自身可以保证程序的正常结束。当await结束时,tomcat所就正常的结束了,包括相应的监听端口等,都已经成功结束。然而,由于项目程序中仍然还有其它线程在运行,所以导致java虚拟机并没有成功的退出。
在我们的项目中,很多时候都运用到了线程。比如,异步调用等。不过,幸运的是,这些线程往往都是守护线程,原因就在于tomcat在运行我们的项目时,对于每一个请求,tomcat是使用了守护线程来进行相应的请求调用,这个保证在以下代码:
- // Start poller threads
- pollers = new Poller[pollerThreadCount];
- for (int i = 0; i < pollerThreadCount; i++) {
- pollers[i] = new Poller(false);
- pollers[i].init();
- Thread pollerThread = new Thread(pollers[i], getName() + "-Poller-" + i);
- pollerThread.setPriority(threadPriority);
- pollerThread.setDaemon(true);
- pollerThread.start();
-
}
[java] view plain copy
- // Start poller threads
- pollers = new Poller[pollerThreadCount];
- for (int i = 0; i < pollerThreadCount; i++) {
- pollers[i] = new Poller(false);
- pollers[i].init();
- Thread pollerThread = new Thread(pollers[i], getName() + "-Poller-" + i);
- pollerThread.setPriority(threadPriority);
- pollerThread.setDaemon(true);
- pollerThread.start();
-
}
所以,一般情况下,在我们的项目代码中使用new Thread建立的线程都是守护线程,原因就是新建线程默认上使用建立线程时的当前线程所处的守护状态。tomcat的请求处理线程为守护线程,所以我们一般情况下建立的线程也是守护线程。然而,Executors除外。
使用Executors建立后台线程并执行一些多线程操作时,Executors会使用相对应的threadFactory来对 runnable建立新的thread,所以使用默认的threadFactory时就会出问题。默认的ThreadFactory强制性的将新创建的线程设置为非守护状态,如下所示:
- public Thread newThread(Runnable r) {
- Thread t = new Thread(group, r,
- namePrefix + threadNumber.getAndIncrement(),
- 0);
- if (t.isDaemon())
- t.setDaemon(false);
- if (t.getPriority() != Thread.NORM_PRIORITY)
- t.setPriority(Thread.NORM_PRIORITY);
- return t;
-
}
[java] view plain copy
- public Thread newThread(Runnable r) {
- Thread t = new Thread(group, r,
- namePrefix + threadNumber.getAndIncrement(),
- 0);
- if (t.isDaemon())
- t.setDaemon(false);
- if (t.getPriority() != Thread.NORM_PRIORITY)
- t.setPriority(Thread.NORM_PRIORITY);
- return t;
-
}
所以,一般情况下,我们使用executors创建多线程时,就会使用默认的threadFactory(即调用只有一个参数的工厂方法),而创建出来的线程就是非守护的。而相应的程序就永远不会退出,如采用Executors创建定时调度任务时,这个调试任务永远不会退出。解决的办法就是重写相对应的 threadFactory,如下所示:
new ThreadFactory() {
public Thread newThread(Runnable r) {
Thread s = Executors.defaultThreadFactory().newThread(r);
s.setDaemon(true);
return s;
}
}
同理,对于java web项目中的线程程序,一定要记住将相应的线程标记为守护线程(尽管它默认就是守护的)。而对于使用Executors,一定要记住传递相应的threadFactory实现,以重写相应的newThread方法,将线程标记为守护线程。
以上的结论对于普通的java项目同样有效,为了正常的结束相应的程序,一定要正确的使用相应的线程,以避免java程序不能退出的问题。
第二种解决方案
建一个监听类,该类实现ServletContextListener接口中的contextInitiialized()方法和contextDestroyed()方法。然后在tomcat中注册该监听类,开启或关闭tomcat时,都将先执行该监听类
1、举例:新建监听类CgmIndexListener
[java] view plain copy
- package com.saas.cgm;
- import javax.servlet.ServletContextEvent;
- import javax.servlet.ServletContextListener;
- //若使用ServletContextListener接口,可能需添加javax.servlet-5.1.12.jar或其它库
- public class CgmIndexListener implements ServletContextListener{
- private static ExecutorService exec = Executors.newCachedThreadPool();//线程池
- //关闭tomcat前关闭线程
- public void contextDestroyed(ServletContextEvent arg0) {
- exec.shutdownNow();;
- }
- //开启tomcat前执行线程
- public contextInitiialized(ServletContextEvent arg0) {
- RamIndexWriter ramIndexWriter = new RamIndexWriter();//新建一个线程
- exec.execute(ramIndexWriter);
-
}
2、注册该监听类:在tomcat安装目录下的conf文件夹内的web.xml文件中添加:
[html] view plain copy
- <listener>
- <listener-class>com.saas.cgm.CgmIndexListener</listener-class>
-
</listener>
3、此时,关闭tomcat时,tomcat首先关闭线程池exec中的ramIndexWriter 线程,当没有子线程在运行时,java.exe进程也就能被顺利关闭了
附件2:http://412887952-qq-com.iteye.com/blog/2292475
过滤器(Filter)文件
com.kfit.filter.MyFilter.java
package com.kfit.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
/**
*
* 使用注解标注过滤器
* @WebFilter将一个实现了javax.servlet.Filter接口的类定义为过滤器
* 属性filterName声明过滤器的名称,可选
* 属性urlPatterns指定要过滤的URL模式,也可使用属性value来声明.(指定要过滤的URL模式是必选属性)
* @author Angel(QQ:412887952)
* @version v.0.1
*/
@WebFilter(filterName="myFilter",urlPatterns="/*")
publicclass MyFilter implements Filter{
@Override
publicvoid init(FilterConfig config) throws ServletException {
System.out.println("过滤器初始化");
}
@Override
publicvoid doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
System.out.println("执行过滤操作");
chain.doFilter(request, response);
}
@Override
publicvoid destroy() {
System.out.println("过滤器销毁");
}
}
ServletContext监听器(Listener)文件
com.kfit.listener.MyServletContextListener:
package com.kfit.listener;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
/**
* 使用@WebListener注解,实现ServletContextListener接口
*
* @author Angel(QQ:412887952)
* @version v.0.1
*/
@WebListener
public class MyServletContextListener implements ServletContextListener {
@Override
public void contextDestroyed(ServletContextEvent arg0) {
System.out.println("ServletContex销毁");
}
@Override
public void contextInitialized(ServletContextEvent arg0) {
System.out.println("ServletContex初始化");
}
}
ServletContext监听器(Listener)文件(HttpSessionListener)
MyHttpSessionListener.java
package com.kfit.listener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
/**
* 监听Session的创建与销毁
*
*/
@WebListener
publicclassMyHttpSessionListenerimplementsHttpSessionListener {
@Override
publicvoid sessionCreated(HttpSessionEvent se) {
System.out.println("Session 被创建");
}
@Override
publicvoid sessionDestroyed(HttpSessionEvent se) {
System.out.println("ServletContex初始化");
}
}
注意不要忘记在 SpringBootSampleApplication.java 上添加 @ServletComponentScan 注解。
启动的过程中我们会看到输出:
ServletContex初始化
过滤器初始化
服务启动后,随便访问一个页面,会看到输出:
执行过滤操作
Session 被创建
为什么无法看到session的过程:http://zhidao.baidu.com/link?url=EP-wlLvKpo8zI5NaIZrESzCdivq3Xg8VgOWQOvfpSLl3opTgvESerpo4wsG6tOs_dm6cQQMF_kQ6THNjNzr2Nq
至于如何使用代码的方式注册Filter和Listener,请参考上一篇文章关键Servlet的介绍。不同的是需要使用FilterRegistrationBean 和 ServletListenerRegistrationBean 这两个类。