扫盲 JVM 安全退出机制:shutdownHook,signalHandler

createh52个月前 (02-01)技术教程14

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 来做相关的逻辑。

相关文章

java小知识-ShutdownHook(优雅关闭)

作者:京东物流 崔冬冬一、先提出一个问题我们如果在JVM退出的时候做一些事情,比如关闭远程链接,怎么实现呢?二、ShutdownHook简介java里有个方法Runtime.getRuntime#ad...

如何优雅地停止Java进程(如何停止java运行)

目录理解停止Java进程的本质应该如何正确地停止Java进程如何注册关闭钩子使用关闭钩子的注意事项信号量机制总结理解停止Java进程的本质我们知道,Java程序的运行需要一个运行时环境,即:JVM,启...

还不理解 Error 和 Exception 吗,看这篇就够了

在 Java 中的基本理念是 结构不佳的代码不能运行,发现错误的理想时期是在编译期间,因为你不用运行程序,只是凭借着对 Java 基本理念的理解就能发现问题。但是编译期并不能找出所有的问题,有一些 N...

使用 kill 命令杀死 java进程,你用对了吗?

原文地址:https://dwz.cn/E88v8sLN作者: 占小狼在本地调试agent相关功能,需要经常性的杀掉Java进程,验证一些极端情况。每次都是本能执行如下步骤jpskill -9rebo...

又一时代结束 甲骨文宣布将弃用Java插件

Java 插件一直被认为是系统不稳定因素的温床,它的时代即将过去。甲骨文公司宣布 Java 插件即将退出历史舞台。Java 插件不会立即死去。它将在下一个版本的 Java 开发工具包中被弃用,然后随着...

Spring正确关闭线程池姿势——优雅停机

前言前几天看到一篇文章,关于线程池关闭的知识点,有点收获;给大家分享一下线程池正确关闭方式我们直接用个案例,公布方法上面就是常规的线程池的使用为简化讨论的复杂性,本文的线程池均是指JDK中的java....