shutdown hook 是 jvm 提供的关闭前回调机制,本质是继承 thread 的非守护线程,用于执行轻量、快速、无依赖的收尾操作,如关闭文件流、发退出信号、清理临时目录等,不保证执行顺序,禁止阻塞、加锁或依赖已卸载资源。

Java 中的 Runtime.getRuntime().addShutdownHook() 并不是“钩子函数”(Hook Function)在操作系统或底层编程意义上的实现,而是一个由 JVM 提供的、用于注册**JVM 关闭前回调逻辑**的机制。它本质是线程对象的封装,其行为受 JVM 生命周期严格约束,使用不当容易引发死锁、资源竞争或挂起等问题。
shutdown hook 是什么?
Shutdown hook 是一个继承自 Thread 的普通线程对象,通过 Runtime.getRuntime().addShutdownHook(hook) 注册后,会在 JVM 正常关闭流程中(如调用 System.exit()、收到 SIGTERM、或所有非守护线程结束)被启动并执行。它不是中断处理、也不是 AOP 增强,更不参与类加载或方法拦截。
关键特性包括:
- JVM 保证 shutdown hook 线程在关闭序列中被执行,但不保证执行顺序(多个 hook 间无先后约定)
- hook 线程默认为非守护线程,因此它的运行会延迟 JVM 真正退出,直到它自己终止
- 一旦 JVM 进入关闭过程,
addShutdownHook将抛出IllegalStateException - 无法通过
removeShutdownHook安全取消已启动的 hook —— 只能移除尚未执行的注册项
哪些场景适合用 shutdown hook?
适用于轻量、快速、无依赖的收尾动作,例如:
- 关闭已打开但未封装进 try-with-resources 的文件流(如日志文件句柄)
- 向监控系统发送“进程即将退出”信号(注意:网络调用需设超时)
- 清理临时目录(仅限本地磁盘,避免跨 NFS 或挂载点异常)
- 将内存缓存中的少量关键状态刷写到磁盘(需控制大小与耗时)
不适合做以下事情:等待远程服务响应、加锁其他业务线程、执行数据库长事务、触发复杂 Spring Bean 销毁逻辑(应交由容器管理)、或尝试重新初始化任何资源。
常见陷阱与规避方式
实际使用中最易踩坑的几个点:
-
阻塞导致 JVM 卡死:hook 中调用了
Thread.join()、Object.wait()或无超时的 I/O,会使 JVM 永久挂起。建议所有阻塞操作必须设置明确超时,并在超时后主动 return - 与业务线程争抢锁:若业务代码持有锁期间 JVM 开始关闭,而 hook 又试图获取同一把锁,就会死锁。应避免在 hook 中访问被业务线程长期持有的共享对象或同步块
- 静态资源已被卸载:关闭过程中,类加载器可能已开始释放,静态字段可能为 null。不要在 hook 中访问不确定生命周期的静态工具类或单例(除非你完全掌控其初始化与存活范围)
-
重复注册或误删:多次 add 同一线程对象会抛
IllegalArgumentException;用removeShutdownHook时务必确保传入的是原始引用,且该 hook 尚未启动
一个安全的使用示例
下面是一个推荐的写法模式:
Thread cleanupHook = new Thread(() -> {
try {
// 快速、幂等、无外部依赖的操作
LogManager.shutdown(); // Log4j2 自带安全关闭
if (tempDir != null && tempDir.exists()) {
FileUtils.deleteQuietly(tempDir);
}
} catch (Throwable t) {
// 不抛异常,不记录复杂日志(logger 可能已不可用)
System.err.println("Cleanup hook failed: " + t.getMessage());
}
});
cleanupHook.setDaemon(false); // 显式声明(虽默认如此)
Runtime.getRuntime().addShutdownHook(cleanupHook);
注意:不建议在 hook 中调用 System.out.println 或任意 logger 实例,因为日志框架本身可能正处于关闭流程中;优先使用 System.err 或直接写文件(需确保文件句柄仍有效)。










