深挖 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),程序运行时可以接收操作系统发送的“信号”,如:
信号 | 名称 | 含义 |
---|---|---|
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
中的代码片段:
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 决定“关闭”时,会执行以下步骤:
- 冻结所有用户线程;
- 执行所有通过
addShutdownHook()
注册的线程; - 执行 finalizers(如果启用了
System.runFinalizersOnExit(true)
,不推荐); - 真正退出进程。
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
- 销毁实现了
@PreDestroy
、DisposableBean
的 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 的关闭流程)
最后
如果文章对你有帮助,点个免费的赞鼓励一下吧!关注公众号:加瓦点灯, 每天推送干货知识!