main方法结束程序不退出排查
saber599

背景

线上环境接手的代码有使用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查看原因

  1. 伪代码
    代码实现的业务功能为:初始化Spring IOC容器获取Spring上下文,使用启动一个bean执行业务代码,最后再退出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Test {
public static void main(String[] args) {
ExecutorBoot boot = null;
try {

ApplicationContext context = new FileSystemXmlApplicationContext(new String[]{"classpath:conf/spring/*.xml", "conf/spring/*.xml", "conf/*.xml"});

boot = context.getBean(ExecutorBoot.class);
boot.execute(datasource);
ExeMode datasource = DpExecutorBoot.getMode(command);
datasource.loadDataInfo(command);
LogWriter.stop(boot.job.getMeta().getId());
LogWriter.stopInitLogger();
if (!boot.execute()) {
System.exit(-1);
} else {
System.exit(0);
}
} catch (Throwable ex) {
LogWriter.stop(boot.job.getMeta().getId());
LogWriter.stopInitLogger();
System.exit(-1);
}
}
}
  1. 使用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()即可

思考

  1. 在使用java -jar执行main方法时,需要保证main方法结束后将所有的线程关闭或者直接调用System.exit()
  2. 在使用Spring框架、线程池的情况下,最好自己指定线程池中的线程名称,后续好排查问题