Java应用启动、定时任务卡死,你不会还不知道怎么分析吧

一、背景

        写了一天代码,临近下班,准备发布到测试环境提测,但部署发现启动一直卡死,启动日志输出到一半不动了,并且打印的日志里啥有效信息都没有,不知道原因,今晚又要加班了,崩溃吧?

        线上定时任务跑到一半突然不动了,你不会只知道重启吧?重启没用,你不会要上紧急版本加日志观察卡在哪里吧?

二、分析工具

        使用jstack命名来定位到造成应用程序卡死的具体代码行数。以下是来自某心某言对jstack的解析。

        jstack是Java虚拟机(JVM)自带的一种堆栈跟踪工具,主要用于生成虚拟机当前时刻的线程快照(也称为threaddump或javacore文件)。线程快照是当前JVM内每一条线程正在执行的方法堆栈的集合,其生成的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。

        当线程出现停顿时,通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。这对于诊断和解决线程相关的问题非常有帮助。

        jstack的命令格式为jstack [option] pid,其中pid是需要被打印配置信息的Java进程ID,可以使用jps工具查询。常用的选项包括:

  • -F:当正常输出的请求不被响应时,强制输出线程堆栈。
  • -m:如果调用到本地方法的话,加上此参数可以显示本地方法的堆栈。
  • -l:除堆栈外,显示关于锁的附加信息,在发生死锁时可以用jstack -l pid来观察锁持有情况。

        此外,如果Java程序崩溃生成core文件,jstack工具可以用来获得core文件的Java stack和native stack的信息,从而可以轻松地知道Java程序是如何崩溃和在程序何处发生问题。同时,jstack工具还可以附属到正在运行的Java程序中,查看当时运行的Java程序的Java stack和native stack的信息,这对于诊断当前运行的Java程序呈现hung的状态非常有用。

三、场景复现——应用启动

        先来看一下正常的启动日志输出,最终会输出启动耗时时长。

        实现一个ApplicationListener,并使其监听上下文刷新完成事件,在onApplicationEvent方法里用死循环来模拟应用启动过程中卡死。也就是在上下文刷新完成后,应用启动马上好了,让程序卡着。

import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;

@Component
public class SleepListener implements ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        while (true) {
            event.getApplicationContext();
        }
    }
}

        加上死循环后,日志只输出到Tomcat启动完成。

 使用jstack来定位

1、查看进程号

2、执行 jstack -l 进程号 > 文件名 将应用进程的线程快照输出到指定文件中,便于查看。

3、查找启动线程对应的线程快照,启动线程名称为“main”(springboot默认启动线程名),打开快照文件,可以清晰的看到启动线程卡在了SleepListener的13行代码处。便可方便得将问题代码定位出来。

四、场景复现——定时任务

1、自定义spring Schedule的线程池,将定时任务线程名称指定为:SchedulePool-thread-%d,便于查询线程对应的线程快照。

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

import java.util.concurrent.*;

@Configuration
public class ScheduleConfig implements SchedulingConfigurer {
    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        ThreadFactoryBuilder threadFactoryBuilder = new ThreadFactoryBuilder();
        threadFactoryBuilder.setNameFormat("SchedulePool-thread-%d");
        ScheduledThreadPoolExecutor taskScheduler = new ScheduledThreadPoolExecutor(2, threadFactoryBuilder.build());
        scheduledTaskRegistrar.setScheduler(taskScheduler);
    }
}

2、定义一个如下定时Job,模拟卡死。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class SleepJob {

    private static final Logger logger = LoggerFactory.getLogger(SleepJob.class);

    @Scheduled(cron = "0 0/1 * * * ?")
    public void run() {
        logger.info("SleepJob start");
        try {
            while (true) {
                Thread.sleep(1000L);
            }
        } catch (Throwable t) {
            logger.info(this.getClass().getName() + "执行异常", t);
        }
        logger.info("SleepJob end");
    }
}

3、应用启动后,只输出了Job入口开始日志

4、使用jstack来定位导致卡死的具体代码行数,当前应用程序的进程号为5939,执行如下命名将当前应用程序进程的线程快照输出到5939.txt文件中。

查看线程快照文件,快速定位到线程名称为"SchedulePool-thread-"的地方,可清晰地看到该线程处于TIME_WAITING状态,以及具体的代码行数。

五、BTW

1、自定义线程、线程池需要为其指定好有明确含义的线程名称。

2、定时任务执行一半,发现不跑了,也可能是执行过程中遇到Error,但只try catch的Exception。所以一些定时任务,独立线程代码,应该把Throwable catch住。

  • 27
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值