1. 概述

启动一个服务通常很简单,但真正考验设计的是——如何优雅地关机。对于运行在 JVM 上的应用来说,资源清理、连接关闭、状态保存等操作,往往需要在进程退出前完成。这时候,Shutdown Hook(关闭钩子) 就派上用场了。

本文将深入探讨 JVM 的终止方式,并通过 Java 提供的 API 来注册和管理 Shutdown Hook。如果你对 System.exit()Runtime.getRuntime().halt() 的区别还不清楚,建议先阅读 Java 中 JVM 关闭机制对比 这篇文章打好基础。

2. JVM 的关闭方式

JVM 的终止分为两类:✅ 正常关闭 和 ❌ 强制终止。

✅ 正常关闭(Controlled Shutdown)

当发生以下任一情况时,JVM 会进入有序的关闭流程:

  • 最后一个非守护线程结束
    比如 main 线程执行完毕,JVM 自动触发关闭流程。

  • 收到操作系统中断信号
    例如用户按下 Ctrl + C,或系统注销时发送的 SIGINT 信号。

  • Java 代码中调用 System.exit(status)
    主动发起退出,status 表示退出状态码(0 通常表示成功)。

在这种模式下,JVM 会执行所有已注册的 Shutdown Hook,确保资源得以释放。

❌ 强制终止(Abrupt Termination)

某些情况下,JVM 来不及做任何清理就会被直接杀掉:

  • 使用 kill -9 <pid> 命令
    发送 SIGKILL 信号,进程无法捕获或处理,立即终止。

  • 调用 Runtime.getRuntime().halt(status)
    这是一个“核按钮”级别的操作,绕过所有清理机制,强制 JVM 停止。

  • 操作系统崩溃或断电
    物理层面的故障,自然无法执行任何 Java 层逻辑。

⚠️ 在这些场景中,Shutdown Hook 不会被执行,所以不能依赖它来保证关键数据的持久化。

3. Shutdown Hook 详解

Shutdown Hook 是 JVM 提供的一种机制,允许我们在 JVM 正常关闭前执行一段自定义逻辑。常见的用途包括:

  • 关闭数据库连接池
  • 刷写缓存到磁盘
  • 停止消息消费者
  • 更新服务注册状态(如从 Eureka 下线)

3.1 注册 Shutdown Hook

通过 Runtime.getRuntime().addShutdownHook(Thread) 方法注册钩子:

Thread printingHook = new Thread(() -> System.out.println("In the middle of a shutdown"));
Runtime.getRuntime().addShutdownHook(printingHook);

运行 System.exit(129); 后输出:

> System.exit(129);
In the middle of a shutdown

说明钩子成功执行。

⚠️ 注意事项

  • 钩子必须是未启动的线程
    如果线程已经 start() 过,再注册会抛异常:
Thread longRunningHook = new Thread(() -> {
    try {
        Thread.sleep(300);
    } catch (InterruptedException ignored) {}
});
longRunningHook.start();

assertThatThrownBy(() -> Runtime.getRuntime().addShutdownHook(longRunningHook))
  .isInstanceOf(IllegalArgumentException.class)
  .hasMessage("Hook already running");
  • 同一个线程不能重复注册
Thread unfortunateHook = new Thread(() -> {});
Runtime.getRuntime().addShutdownHook(unfortunateHook);

assertThatThrownBy(() -> Runtime.getRuntime().addShutdownHook(unfortunateHook))
  .isInstanceOf(IllegalArgumentException.class)
  .hasMessage("Hook previously registered");

✅ 正确做法:每个钩子应是一个独立的、未启动的 Thread 实例。

3.2 移除 Shutdown Hook

如果某个钩子不再需要执行,可以动态移除:

Thread willNotRun = new Thread(() -> System.out.println("Won't run!"));
Runtime.getRuntime().addShutdownHook(willNotRun);

assertThat(Runtime.getRuntime().removeShutdownHook(willNotRun)).isTrue();

removeShutdownHook() 返回 true 表示移除成功;若 JVM 已经开始关闭流程,则返回 false

这个功能在某些动态场景下很有用,比如插件系统中某个模块被卸载时取消其清理逻辑。

3.3 使用陷阱与限制

❌ 钩子只在正常关闭时生效

如前所述,以下情况钩子不会执行

Thread haltedHook = new Thread(() -> System.out.println("Halted abruptly"));
Runtime.getRuntime().addShutdownHook(haltedHook);
        
Runtime.getRuntime().halt(129); // ❌ 钩子不会运行

halt() 方法直接终止 JVM,不触发任何钩子或 finalize。

⚠️ 钩子执行顺序不确定

多个钩子之间没有执行顺序保证,所以不要设计依赖顺序的清理逻辑。

⚠️ 避免长时间阻塞

虽然钩子线程可以执行任意逻辑,但应尽量避免:

  • 长时间 IO 操作
  • 等待锁或网络响应
  • 死循环或无限重试

否则可能导致 JVM 关闭延迟,甚至被外部强制 kill。

✅ 推荐模式:设置超时 + 守护线程协作

Thread cleanupHook = new Thread(() -> {
    System.out.println("Starting cleanup...");

    // 使用带超时的清理逻辑
    boolean success = performGracefulShutdown(5_000); // 最多等5秒
    if (!success) {
        System.err.println("Cleanup timed out, forcing exit.");
    }

    System.out.println("Cleanup completed.");
});

Runtime.getRuntime().addShutdownHook(cleanupHook);

4. 总结

Shutdown Hook 是实现 JVM 应用优雅关闭的简单粗暴但有效的手段。掌握它的使用场景和限制,能帮你避免很多“进程一杀数据就丢”的踩坑经历。

关键点回顾:

✅ 仅在正常关闭时触发
❌ 不响应 kill -9halt()
✅ 可动态添加/移除
⚠️ 执行顺序无保障,避免阻塞

示例代码已托管至 GitHub:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-jvm-2


原始标题:Adding Shutdown Hooks for JVM Applications