背景
线上环境接手的代码有使用shell命令通过java -jar直接拉起一个java进程执行main方法的场景,系统长时间运行后发现有拉起的java进程main执行完毕后没有销毁
排查过程
1. 查看日志
通过日志看到main方法中有java.lang.NullPointerException异常出现,但是正常出现异常后main线程结束后,进程会被销毁,现在整个进程没被销毁,猜测还有除了main线程的其他非守护线程存活
2. 使用arthas查看未销毁进程中线程存活情况
从图中能看出,确实还有两个非守护线程存活,DestroyJavaVM线程是等待其他线程执行完毕后销毁进程的线程,该线程状态是正常的,但是图中还有一个poo1-2-thread-1线程还存活,从名称上无法确定该线程正在执行的业务,但是已经能证实之前的猜测了,就是因为还有除main线程的其他非守护线程存活导致进程未销毁,接下来就是明确线程是用于哪个业务
3. 查看业务代码,有没有使用线程池
经排查后没发现有业务在使用线程池
4. 将业务代码抽象出伪代码,本地debug查看原因
- 伪代码
代码实现的业务功能为:初始化Spring IOC容器获取Spring上下文,使用启动一个bean执行业务代码,最后再退出
1 | public class Test { |
- 使用idea本地debug查看堆栈信息
从图中可以发现poo1-2-thread-1线程是ScheduledThreadPoolExecutor线程池中的线程,在获取task时,队列中没有task就会一直等待,从这还是无法确定具体是从哪个业务创建的线程池
3. 确定ScheduledThreadPoolExecutor创建位置
在ScheduledThreadPoolExecutor打个断点,重启再看下从哪创建的
从图中可以看出ScheduledThreadPoolExecutor是由Spring创建的,有兴趣的同学可以去看Spring的源码,打上@EnableScheduling注解后会导入SchedulingConfiguration从而注入图中堆栈信息的ScheduledAnnotationBeanPostProcessor。在Spring刷新上下完成后就会发出ContextRefreshedEvent事件,ScheduledAnnotationBeanPostProcessor会接受该事件进行task生成以及ScheduledThreadPoolExecutor生成等工作
问题修复
调整catch里面的内容,保证无NullPointerException产生,并且保证无论是否出现异常都调用System.exit()即可
思考
- 在使用java -jar执行main方法时,需要保证main方法结束后将所有的线程关闭或者直接调用System.exit()
- 在使用Spring框架、线程池的情况下,最好自己指定线程池中的线程名称,后续好排查问题