JVM的ShutdownHook机制
前言
在看Spring应用上下文生命周期时,其中AbstractApplicationContext抽象类中有个registerShutdownHook方法,提到了Runtime.getRuntime().addShutdownHook(),顺便记录下。
介绍
Runtime.getRuntime().addShutdownHook() 通过给jvm增加一个钩子,当jvm进行关闭时会调用添加的Thread任务,自定义清除工作,使得jvm能够进行优雅的关闭。
如果在某些场景下强制退出,例如使用kill -9可能会导致一些问题,如下:
1、缓存中数据尚未持久化,致使数据丢失
2、异步任务未执行完成,致使任务丢失
3、正在写入的文件,没有更新完成,致使文件损坏
示例
模拟jvm退出,保证线程池提交的任务执行完成。
创建测试类ShutdownHookTest
public class ShutdownHookTest {
private static ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3);
static {
// jvm关闭的钩子
Runtime.getRuntime().addShutdownHook(new Thread(() -> close()));
}
/**
* 回调关闭
*/
private static void close() {
System.out.println("======start close======");
executorService.shutdown();
System.out.println("executorService#shutdown");
try {
// 最多等10秒,awaitTermination return true if this executor terminated and false if the timeout elapsed before termination
System.out.println("executorService#awaitTermination " + executorService.awaitTermination(10000, TimeUnit.SECONDS));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("======end close======");
}
public static void main(String[] args) throws Exception {
// 测试kill -9的情况,直接结束进程,ShutdownHook得不到执行
displayProcessId(ShutdownHookTest.class);
// 每1秒提交一个3秒后执行的任务
for (int i = 0 ; i < 10; i ++) {
Thread.sleep(1000);
final int index = i;
// 3s后执行任务 先shutdown再terminated
if (!executorService.isShutdown()) {
System.out.println("add execute " + index);
executorService.schedule(() -> System.out.println("execute " + index),
3L, TimeUnit.SECONDS);
}
// 第5秒时,关闭进程
if (i == 5) {
System.exit(0);
}
}
}
public static void displayProcessId(Class clazz) throws Exception {
// 获取监控主机
MonitoredHost local = MonitoredHost.getMonitoredHost("localhost");
// 取得所有在活动的虚拟机集合
Set<?> vmList = new HashSet<Object>(local.activeVms());
// 遍历集合,输出PID和进程名
for(Object process : vmList) {
MonitoredVm vm = local.getMonitoredVm(new VmIdentifier("//" + process));
// 获取类名
String processName = MonitoredVmUtil.mainClass(vm, true);
if (clazz.getName().equals(processName)) {
System.out.println(process + " ------> " + processName);
}
}
}
}
示例中,通过Runtime.getRuntime().addShutdownHook(new Thread(() -> close()));添加ShutdownHook任务,当jvm关闭时,回调close方法。
close方法会进行线程池的关闭,同时等待10s,使得未执行的任务留有10s时间进行执行。
在主方法中,「每1秒提交一个3秒后执行的任务」,预创建10个,当创建第5个时,调用System.exit(0);关闭应用进程。
输出结果如下:
add execute 0
add execute 1
add execute 2
execute 0
add execute 3
execute 1
add execute 4
execute 2
add execute 5
======start close======
executorService#shutdown
execute 3
execute 4
execute 5
executorService#awaitTermination true
======end close======
另外,通过displayProcessId方法,获取到进程id,通过kill -9方法结束,此时shutdownHook任务得不到执行。
扩展
jvm关闭的常见方式,正常和异常关闭会回调shutdownHook任务。
总结
平时写crud关注的比较少,一般运维层面,会在某应用重启前,先将流量切到其它应用(副本),再等待一段时间后关闭并重启该应用,相当于给应用一个关闭时间,执行未完成的线程任务。
这个时长不好控制,可能30-60s,也可以结合添加shutdownHook进行资源或任务校验。或者一些场景,例如jvm关闭时清除临时资源、通知等等。