JProfile版本:11.1.4
项目中有一个数据库定时备份任务,备份了几天之后,发现停止备份了,查系统运行日志未发现异常。重启操作系统后又可以备份了,但同样也是备份几天后停止。怀疑是quartz线程池的线程不够用,于是增加了线程数量,但治标不治本,定时任务最后还是停止的。
前期使用JDK提供的jvisualvm观察线程状态未发现异常,后期改成JProfile。
JProfile使用
安装就不介绍了,安装完成后,需要的一步是用KeyGen.exe生成秘钥,才能破解JProfile。
tomcat启动完成后,双击桌面上的JProfie图标,进入下面这个界面,点第二个选项
查看服务,会自动搜索出本地正在运行的JVM
选择我们的服务的pid,服务pid可通过任务管理器查,选中我们服务对应的pid,再点击 Start
采用推荐的Sampling模式 ,此模式并没有启用JProfile的所有功能,给CPU、应用程序带来比较低的开销
此时,我们就进入了概览视图
Threads中的Thread History是在排查的过程中常用的,可用来查看线程的状态。
定位问题
在我们的项目中,使用Quartz调度框架来管理定时任务,Quartz线程池名前缀为:QuartzScheduler_worker
在搜索栏中搜索QuartzScheduler_worker就可以看到Quartz线程。
触发数据库定时备份任务,在备份完成后,发现线程并没有进入死亡、等待或者阻塞状态,而是一直处于运行状态(Runnable)。触发5次后,QuartzScheduler_Worker五个线程全部处于运行状态,线程全部被占用完,新来的定时任务拿不到线程就无法执行。在这种情况下,无论配多少线程,长期运行后都会把Quartz线程池中的线程资源耗尽。
排查业务代码发现代码中需要执行一个数据库备份的bat脚本,通过Runtime去执行bat脚本后返回一个Process对象,调用Process对象的waitFor方法,这个方法等待bat脚本执行结束。问题就出在waitFor方法这里,实测下来bat脚本已经执行结束,备份路径也能看到备份的sql文件,但waitFor方法没有感知到脚本执行结束,它就一直等待,无法继续往下走后续业务代码,
Process ps = Runtime.getRuntime().exec(tempFile.getAbsolutePath());
ps.waitFor();
解决方案
定位到问题后,解决问题的基本思路是:
- 在bat脚本末尾输出一个字符串,这个字符串在备份业务执行之后才会输出,当输出这个字符串时,表示sql备份已经完成。
- 创建一个工作线程Worker,它持有Process对象,Process对象可以理解为一个cmd命令窗口,Worker线程获取Process对象的ErrorStream和InputStream流对象实例。
- 创建流内容读取类,用来读取两个流对象实时输出的内容,当流中输出的内容包含我们指定的字符串时,在业务代码中调用Process类的destroy方法主动销毁Process的实例。
经过实测,这个方案的确达到了等定时备份脚本执行结束后释放线程资源的目的。下面贴一份解决代码:
package com.xxx.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeoutException;
public class RuntimeProcessUtil {
public final static Logger logger = LoggerFactory.getLogger(RuntimeProcessUtil.class);
// bat脚本执行完成标识,执行完成后会输出这个内容
public final static String COMPLETION_FLAG = "DatabaseBackupCompleted";
public static void main(String[] args) throws InterruptedException, IOException, TimeoutException {
int status;
long timeout = 30 * 1000;
String command = "D:\\1-Assert-Test\\Nice.bat";
status = executeProcess(timeout, command);
// status = executeProcess(timeout, null, commands);
logger.info("Runtime的Process子进程运行结束,程序状态是:{}", status);
}
public static int executeProcess(final long timeout, String command) throws IOException, TimeoutException, InterruptedException {
Process process = Runtime.getRuntime().exec(command);
Worker worker = new Worker(process);
worker.start();
try {
worker.join(timeout);
if (worker.exit != null) {
return worker.exit;
} else {
throw new TimeoutException();
}
} catch (InterruptedException | TimeoutException e) {
worker.interrupt();
Thread.currentThread().interrupt();
throw e;
} finally {
process.destroy();
}
}
public static int executeProcess(final long timeout, File dir, final String[] command) throws TimeoutException, InterruptedException, IOException {
Process process = Runtime.getRuntime().exec(command);
Worker worker = new Worker(process);
worker.start();
try {
worker.join(timeout);
if (worker.exit != null) {
return worker.exit;
} else {
throw new TimeoutException();
}
} catch (InterruptedException e) {
worker.interrupt();
Thread.currentThread().interrupt();
throw e;
} finally {
process.destroy();
}
}
public static class Worker extends Thread {
private final Process process;
private Integer exit;
private Worker(Process process) {
this.process = process;
}
@Override
public void run() {
InputStream errorStream = null;
InputStream inputStream = null;
try {
errorStream = process.getErrorStream();
inputStream = process.getInputStream();
readStreamInfo(process, errorStream, inputStream);
exit = process.waitFor();
process.destroy();
} catch (InterruptedException ignore) {
return;
}
}
}
/**
* 读取RunTime.exec运行子进程的输入流 和 异常流
*
* @param inputStreams
*/
public static void readStreamInfo(Process process, InputStream... inputStreams) {
ExecutorService executorService = Executors.newFixedThreadPool(inputStreams.length);
for (InputStream in : inputStreams) {
executorService.execute(new BatScriptInputStreamReader(in, process));
}
executorService.shutdown();
}
/**
* bat脚本输入流读取器
*/
public static class BatScriptInputStreamReader implements Runnable {
rivate InputStream in;
private Process process;
public BatScriptInputStreamReader(InputStream in, Process process) {
this.in = in;
this.process = process;
}
@Override public void run() {
String line = null;
try (BufferedReader br = new BufferedReader(new InputStreamReader(in, "GBK"))) {
while ((line = br.readLine()) != null) {
logger.info("------ buffer string: {} ", line);
if (COMPLETION_FLAG.equals(line)) {
process.destroy();
logger.info("成功销毁Runtime.getRuntime().exec()中的Process子进程");
}
}
} catch (IOException e) {
logger.error("读取bat脚本输出的内容异常,", e);
}
}
}
}
参考
Runtime 调用Process.waitfor导致的阻塞问题
https://janche.github.io/2019/06/16/Runtime-%E8%B0%83%E7%94%A8Process-waitfor%E5%AF%BC%E8%87%B4%E7%9A%84%E9%98%BB%E5%A1%9E%E9%97%AE%E9%A2%98/
JProfiler11使用教程之JVM调优
https://blog.csdn.net/weixin_45203607/article/details/123473969
JProfile 11安装教程
https://www.cnblogs.com/zhangxl1016/articles/16220183.html