扫盲 JVM 安全退出机制:shutdownHook,signalHandler
1. 背景
线上跑的 Java 服务,总有退出的时候,而且还很频繁(想想每天服务发布多少次吧,每次发布 JVM 都会退出再重启或者干脆换一台机器启动)。
那么思考下,如果 JVM 退出的时候,有以下问题怎么办:
- 这个时候如果还有在执行中的异步任务,这些任务怎么办?
- 正在写文件呢,写到一半 JVM 退出了,会导致文件损坏或不完整
- 缓存中的数据尚未持久化到磁盘中,导致数据丢失
- ... ...
如果有这些问题,就要考虑 JVM 安全退出了:在JVM 退出的时候做一些善后工作。
关键字:JVM 安全退出;shutdownHook;signalHandler
2. JVM 安全退出场景
JVM 退出有三类场景,如下:
这三类场景中,正常关闭和异常关闭,JVM 可以感知。可以通过 ShutdownHook 或者 SignalHandler 做一些善后工作。
强制关闭 JVM 则感知不到,无法做善后的工作,退出后会造成哪些影响都很难预估,所以我们日常不推荐使用 kill -9 来关闭程序。
3. kill 命令
先了解下 JVM 退出的命令。
服务发布到线上后,肯定没有 IDEA 的关闭按钮给我们用,让我们关闭程序。所以都是通过 kill 命令来关闭 JVM 进程的。
所以我们先了解下 kill 命令。
本地用这个按钮关闭JVM 程序,线上环境只能使用命令行 kill 来关闭了。
kill 的常规用法是:kill -signal_number pid。比如我要强制关闭进程 id(pid)为 49736 的进程, 则执行:kill -9 49736。
常用的 signal_number 有如下几种,其中 2、9、15 用的最多
- 1 HUP (hang up):挂起线程
- 2 INT (interrupt):中断线程(同 ctrl + c)
- 3 QUIT (quit):退出线程(同 ctrl + \)
- 9 KILL (non-catchable, non-ignorable kill):强制终止,线上慎用
- 15 TERM (software termination signal):正常终止
- 19 STOP:暂停线程(同 ctrl+z)
4. SignalHandler
Java 提供 SignalHandler 来监听 kill 信号。我们可以在 SignalHandler 中做善后逻辑。
举个栗子,来看如下代码,是一个监听 INT(kill -2) 信号的信号处理器
java复制代码public static void main(String[] args) throws InterruptedException {
Signal sig = new Signal("INT"); // kill -2 ${pid}
Signal.handle(sig, (s) -> {
System.out.println("Signal handle start");
try {
TimeUnit.SECONDS.sleep(3); // 模拟善后逻辑,阻塞3秒
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Signal handle end");
System.exit(0); // 这里显示调用 exit 退出程序,如果不调用,JVM 实例不会退出,这段逻辑就相当于一个普通的信号处理器
});
Thread.sleep(6000000);
}
程序启动后,我们开启命令行工具,通过 jps 定位 JVM 进程的 pid。再执行 kill 命令:kill -2 ${pid}。
会发现程序触发了 SignalHandler 里面的逻辑,阻塞 3 秒后执行 System.exit(0),程序退出。
如果我们执行其他的kill命令,程序则不会有什么特殊的动作,正常终止。
另外,SignalHandler 不能监听 KILL(kill -9)信号,会报错。
java复制代码Signal sig = new Signal("KILL");
// 执行就会抛异常:
Exception in thread "main" java.lang.IllegalArgumentException: Signal already used by VM or OS: SIGKILL
at sun.misc.Signal.handle(Signal.java:166)
at cn.edu.jxau.blog.Main.main(Main.java:19)
所以,我们可以使用 SignalHandler 监听 kill 信号(线上一般是使用 INT 或者 TERM)做善后逻辑,做到 JVM 安全退出
5. ShutdownHook
要做到 JVM 安全退出,除了 SignalHandler,还可以使用 ShutdownHook。
SignalHandler 触发条件是注册了对应的 kill 信号,有信号才触发。ShutdownHook 花样就比较多了,它本质上是一个JVM 退出的钩子,只要退出就会触发这个钩子。 有以下场景会触发 ShutdownHook:
- System.exit(0);
- IDEA 手动关闭
- 命令行:ctrl+c(注意,ctrl+z 不会触发 shutdownhook)
- 命令行:kill -2 信号
- 命令行:kill -15 信号
- JVM 异常退出(OOM啥的)
这里举一个 JVM 异常退出的栗子。
代码如下,先注册一个 shutdownHook,然后一个 for 循环,模拟程序跑到一半抛出 OutOfMemoryError 的 case。
java复制代码 public static void main(String[] args) throws InterruptedException {
// 注册 shutdown hook
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("shutdownHook start");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("shutdownHook end");
}));
for (int i = 0; i <= 5; i++) {
if (i == 5) {
throw new OutOfMemoryError("123");
}
Thread.sleep(1000);
System.out.println("processing...");
}
}
控制台日志如下,可以看到抛出了 OutOfMemoryError,shutdownHook 也触发了。
所以,我们可以也使用 ShutdownHook做善后逻辑,做到 JVM 安全退出。
那么这两个,到底用哪个?推荐使用 ShutdownHook。因为 ShutdownHook 能够应该更多的退出场景,像异常退出、System.exit()等,都会触发 ShutdownHook 而不会触发 SignalHandler。
6. 注意事项
ShutdownHook 用的比较多,我们看下都有哪些注意事项。
- shutdownHook 的内部逻辑不能调用 System.exit(),否则程序会卡死
- 当存在多个 shutdownHook 时,这几个 shutdownHook 是并发调用的,不保证先后执行顺序
- kill -9 关闭的 JVM 进程,不会触发 shutdownHook,也无法用 SignalHandler 捕获
7. 总结
JVM 退出有三类场景:正常退出、异常退出、强制退出。
Java 对 JVM 退出做善后逻辑提供了两套 API:SignalHander,ShutdownHook。ShutdownHook 因为应对场景更多,所以推荐使用 ShutdownHook 来做相关的逻辑。