深挖 JVM 关闭钩子与 Signal 机制:优雅停机背后的秘密

JVM 如何响应 kill 信号?我们平时注册的 Runtime.getRuntime().addShutdownHook() 究竟是如何执行的?这篇文章带你从源码层深入理解 JVM 的信号处理机制与优雅停机设计。


现实中的场景:为什么要"优雅停机"?

在日常开发中,我们经常会遇到这些需求:

  • 微服务部署在 K8s,POD 被 kill 时要释放资源;
  • 定时任务中断前需要保存进度;
  • IM 服务需要向其他服务发出"我下线了"的通知。

这些行为的触发,往往依赖 JVM 提供的"关闭钩子(Shutdown Hook)"机制。


什么是 Shutdown Hook?

Java 提供了一个方式,在 JVM 即将退出时注册一段"善后代码",用来清理资源、保存数据等。使用方式非常简单:

java 复制代码
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    System.out.println("程序即将关闭,释放资源中...");
}));

当 JVM 收到如 SIGINT(Ctrl+C)、SIGTERM(kill)、System.exit() 等退出信号时,就会执行所有已注册的钩子线程。


JVM 是如何捕获信号的?

操作系统的信号机制简述

在类 Unix 系统中(如 Linux、macOS),程序运行时可以接收操作系统发送的"信号",如:

信号 名称 含义
2 SIGINT Ctrl+C 发送的中断信号
15 SIGTERM kill 默认发送的终止信号
9 SIGKILL kill -9 强制杀死,不可捕获
1 SIGHUP shell 退出后通知子进程关闭

JVM 启动时会注册自己的信号处理器,以便拦截上述信号并优雅退出。


JVM Signal 处理源码分析

在 HotSpot 中,有一部分专门负责处理 OS 信号的代码,位于 src/hotspot/os/ 目录下(不同平台分别实现)。

以 Linux 为例,os_linux.cpp 中的代码片段:

cpp 复制代码
SignalHandler(int sig, siginfo_t* info, void* uc) {
    // 判断信号类型
    if (sig == SIGTERM || sig == SIGINT || sig == SIGHUP) {
        // 调用 JVM 层的 shutdown hook 执行函数
        JVM_HandleSignal(sig);
    }
    ...
}

JVM_HandleSignal 最终会触发 JVM 的"退出路径",包括:

  • 停止所有非守护线程
  • 执行 shutdown hooks
  • 调用 exit 函数

Shutdown Hook 执行流程解析

当 JVM 决定"关闭"时,会执行以下步骤:

  1. 冻结所有用户线程
  2. 执行所有通过 addShutdownHook() 注册的线程
  3. 执行 finalizers(如果启用了 System.runFinalizersOnExit(true),不推荐)
  4. 真正退出进程

JVM 会创建一个内部线程,按注册顺序串行执行所有 Hook 线程。注意:

  • 如果有某个 Hook 卡住(阻塞不退出),JVM 就不会退出;
  • Hook 的执行顺序无法保证,也不能互相依赖;
  • Hook 抛出异常不会影响其他 Hook。

特殊场景下 Shutdown Hook 不会触发?

是的,有两个场景要特别注意:

kill -9 <pid>(发送 SIGKILL)

这个信号无法被任何程序捕获或拦截,直接强制杀死进程,Hook 不会执行。

System.halt(0)

这个方法会绕过所有 ShutdownHook 和 finalizer,直接终止进程:

java 复制代码
Runtime.getRuntime().halt(0);  // 无情终止,无人能救

实战:一个优雅停机的例子

java 复制代码
public class GracefulShutdownDemo {
    public static void main(String[] args) throws Exception {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("服务正在关闭,请稍候...");
            try {
                Thread.sleep(2000);
                System.out.println("清理完成,退出成功!");
            } catch (InterruptedException e) {
                System.out.println("关闭被打断!");
            }
        }));

        System.out.println("服务已启动,PID: " + ProcessHandle.current().pid());

        while (true) {
            Thread.sleep(1000);
        }
    }
}

✅ 测试方式:

  • 执行 java GracefulShutdownDemo
  • 再运行 kill <pid>,可以看到优雅退出日志。
  • 如果是 kill -9 <pid>,钩子不会执行。

Spring Boot 中的优雅停机机制

Spring Boot 封装了 JVM Hook,并在其中触发 ApplicationContext 的关闭。

java 复制代码
SpringApplication application = new SpringApplication(MyApp.class);
application.setRegisterShutdownHook(true); // 默认就是 true
application.run(args);

Spring 停机流程:

  • 执行 ApplicationContext.close()
  • 发布 ContextClosedEvent
  • 销毁实现了 @PreDestroyDisposableBean 的 Bean
  • 自动释放资源(线程池、连接池等)

注册停机逻辑的方式

实现 DisposableBean

java 复制代码
@Component
public class ShutdownCleanup implements DisposableBean {
    @Override
    public void destroy() throws Exception {
        System.out.println("释放数据库连接...");
    }
}

使用 @PreDestroy

java 复制代码
@Component
public class MyComponent {
    @PreDestroy
    public void onExit() {
        System.out.println("正在清理缓存...");
    }
}

监听关闭事件

java 复制代码
@Component
public class ShutdownListener {
    @EventListener
    public void onContextClosed(ContextClosedEvent event) {
        System.out.println("收到 Spring 上下文关闭事件!");
    }
}

Kubernetes 中的容器优雅停机流程

K8s 关闭 Pod 的流程:

text 复制代码
1. 调用 preStop(若配置)
2. 发送 SIGTERM 给主进程(如 java)
3. JVM 执行 Shutdown Hook
4. Spring Boot 执行销毁逻辑
5. 等待 terminationGracePeriodSeconds 超时或进程退出
6. 若未退出,则发送 SIGKILL 强制杀死

配置 preStop 调用接口

yaml 复制代码
lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "curl http://localhost:8080/offline"]

Spring Boot 提供接口:

java 复制代码
@RestController
public class ShutdownController {
    @PostMapping("/offline")
    public void offline() {
        System.out.println("开始下线流程...");
        // 通知注册中心下线、设置健康状态等
    }
}

设置 terminationGracePeriodSeconds

yaml 复制代码
spec:
  terminationGracePeriodSeconds: 30

这个值决定 Pod 终止前等待应用优雅退出的时间。

在生产系统中的使用建议

建议 原因说明
Hook 内代码要尽快执行完 Hook 阻塞会阻塞 JVM 退出
不建议执行复杂逻辑、远程调用等 超时不可控,容易阻塞退出
避免多个钩子间的执行依赖 JVM 不保证执行顺序
尽量使用守护线程来处理非 Hook 的工作项 避免非守护线程阻止 JVM 退出
Kubernetes 中使用 preStop 配合 Hook 保证容器关闭前先运行 Hook
  • JVM 的优雅停机机制其实设计得非常完善,它通过捕捉操作系统信号并执行关闭钩子,为我们提供了资源清理与通知下线的"最后机会"。理解这套机制,对构建可靠、高可用的服务系统至关重要。

延伸阅读推荐:

  • JVM 对信号的全处理链路图
  • sun.misc.Signal 的手动注册方式
  • Spring Boot 中的优雅停机机制底层实现(ApplicationContext 的关闭流程)

最后

如果文章对你有帮助,点个免费的赞鼓励一下吧!关注公众号:加瓦点灯, 每天推送干货知识!

相关推荐
2025学习1 小时前
Spring循环依赖导致Bean无法正确初始化
后端
l0sgAi1 小时前
最新SpringAI 1.0.0正式版-实现流式对话应用
后端
parade岁月1 小时前
从浏览器存储到web项目中鉴权的简单分析
前端·后端
用户91453633083912 小时前
ThreadLocal详解:线程私有变量的正确使用姿势
后端
用户4099322502122 小时前
如何在FastAPI中实现权限隔离并让用户乖乖听话?
后端·ai编程·trae
阿星AI工作室2 小时前
n8n教程:5分钟部署+自动生AI日报并写入飞书多维表格
前端·人工智能·后端
郝同学的测开笔记2 小时前
深入理解 kubectl port-forward:快速调试 Kubernetes 服务的利器
后端·kubernetes
Ray663 小时前
store vs docValues vs index
后端
像污秽一样3 小时前
软件开发新技术复习
java·spring boot·后端·rabbitmq·cloud
Y_3_73 小时前
Netty实战:从核心组件到多协议实现(超详细注释,udp,tcp,websocket,http完整demo)
linux·运维·后端·ubuntu·netty