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

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

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


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

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

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

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


什么是 Shutdown Hook?

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

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

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


JVM 是如何捕获信号的?

操作系统的信号机制简述

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

信号名称含义
2SIGINTCtrl+C 发送的中断信号
15SIGTERMkill 默认发送的终止信号
9SIGKILLkill -9 强制杀死,不可捕获
1SIGHUPshell 退出后通知子进程关闭

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


JVM Signal 处理源码分析

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

以 Linux 为例,os_linux.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,直接终止进程:

Runtime.getRuntime().halt(0);  // 无情终止,无人能救

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

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 的关闭。

SpringApplication application = new SpringApplication(MyApp.class);
application.setRegisterShutdownHook(true); // 默认就是 true
application.run(args);

Spring 停机流程:

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

注册停机逻辑的方式

实现 DisposableBean
@Component
public class ShutdownCleanup implements DisposableBean {
    @Override
    public void destroy() throws Exception {
        System.out.println("释放数据库连接...");
    }
}
使用 @PreDestroy
@Component
public class MyComponent {
    @PreDestroy
    public void onExit() {
        System.out.println("正在清理缓存...");
    }
}
监听关闭事件
@Component
public class ShutdownListener {
    @EventListener
    public void onContextClosed(ContextClosedEvent event) {
        System.out.println("收到 Spring 上下文关闭事件!");
    }
}

Kubernetes 中的容器优雅停机流程

K8s 关闭 Pod 的流程:

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

配置 preStop 调用接口

lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "curl http://localhost:8080/offline"]

Spring Boot 提供接口:

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

设置 terminationGracePeriodSeconds

spec:
  terminationGracePeriodSeconds: 30

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

在生产系统中的使用建议

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

延伸阅读推荐:

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

最后

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值