shutdownhook仅在jvm正常关闭时触发,如system.exit()、sigterm或容器优雅终止;kill-9、崩溃、断电等不触发,且无执行顺序保证,不可依赖耗时i/o或跨进程状态兜底。

shutdownHook 什么时候会触发?
只有 JVM 正常关闭时才执行,比如调用 System.exit()、收到 SIGTERM(Linux 下 kill -15)、或进程被容器平台优雅终止。强制杀进程(kill -9)、JVM 崩溃、断电——这些统统不触发。
常见错误现象:本地测试时用 Ctrl+C 看起来能触发,但部署到 Kubernetes 后钩子完全没跑,大概率是容器配置没发 SIGTERM,或者应用启动脚本里加了 exec java ... 缺失信号转发。
- Spring Boot 默认启用
spring.lifecycle.timeout-per-shutdown-phase=30s,超时后直接中断钩子线程 - 多个钩子无执行顺序保证,别写相互依赖的清理逻辑
- 钩子线程默认是
non-daemon,但不能起新线程再等它结束——新线程可能被直接杀死
addShutdownHook 为什么不能放耗时操作?
JVM 关闭过程本身不等待钩子完成,只是“启动”它们;一旦所有非 daemon 线程退出(包括钩子线程),JVM 就收摊。所以阻塞型 I/O、网络请求、数据库 commit 等,极大概率被截断。
典型翻车场景:shutdownHook 里调 httpClient.close() 却没设超时,底层连接池关不掉,线程卡住,整个 JVM 拖延甚至卡死在关闭阶段。
立即学习“Java免费学习笔记(深入)”;
- 所有 I/O 操作必须带明确超时,比如
socket.setSoTimeout(2000) - 避免在钩子里做日志刷盘(
log4j2的AsyncLoggerContext可能还在队列里) - 优先用内存标记 + 异步落库,而不是关机时同步写 DB
如何安全地释放 Netty 或 NIO 资源?
Netty 的 EventLoopGroup、Channel、Bootstrap 都不是线程安全的,且内部状态在关闭过程中快速变化。直接在钩子里调 group.shutdownGracefully() 是对的,但必须配合 await:
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
if (eventLoopGroup != null && !eventLoopGroup.isTerminated()) {
try {
eventLoopGroup.shutdownGracefully()
.await(3, TimeUnit.SECONDS); // 必须 await,否则白搭
} catch (InterruptedException ignored) {}
}
}));
容易踩的坑:await() 抛 InterruptedException 后没重置中断状态,导致后续清理逻辑跳过;或者 await(3) 返回 false 后没做兜底强制关闭。
-
shutdownGracefully()不等于立刻释放,它先拒绝新任务,再等已有任务完成 - NIO 的
Selector.close()在多线程竞争下可能抛CancelledKeyException,要捕获 - 别在钩子里重新初始化资源(比如 new Channel),JVM 已经禁止新 class 加载
Spring Boot 里要不要手动注册 shutdownHook?
不用。Spring Boot 2.3+ 内置了 GracefulShutdown,只要配 server.shutdown=graceful,它会在收到 SIGTERM 后自动停止 Web 容器,并等待 @PreDestroy 和 SmartLifecycle.stop() 完成。
手动加 Runtime.getRuntime().addShutdownHook() 反而容易和 Spring 的关闭流程冲突,比如重复关闭同一个线程池。
- 优先用
@PreDestroy注解在@Component上做实例级清理 - 全局资源(如共享连接池)适合放在
SmartLifecycle实现类里统一管理 - 如果用了
EmbeddedWebServerFactory自定义 Tomcat,记得调setGracefulShutdownTimeout()
真正难处理的是跨进程边界的状态,比如已发出去但没确认的 MQ 消息、正在写入的本地临时文件——这些没法靠钩子兜底,得靠业务层幂等和外部系统协作。










