java进程通过runtime.addshutdownhook()响应sigterm信号,而非直接处理posix信号;它在jvm关闭流程中执行轻量清理,如关闭socket、db连接池、kafka消费者等独占资源,但需避免阻塞、死锁和依赖未同步状态。

Java进程怎么收到SIGTERM信号
Java本身不直接暴露POSIX信号处理接口,但通过Runtime.addShutdownHook()能响应JVM收到的终止信号(包括SIGTERM)。注意:这**不是**注册信号处理器,而是监听JVM进入关闭流程的时机。Linux下kill -15 $PID、Kubernetes的terminationGracePeriodSeconds结束阶段、Docker stop默认行为,都会触发它。
常见错误现象:
– 直接用Signal.handle()(已废弃且仅限部分JVM)试图捕获SIGTERM,结果无响应或抛UnsupportedOperationException
– 在ShutdownHook里执行阻塞操作(如等待网络请求完成),导致JVM卡住超时被强杀(SIGKILL)
- ShutdownHook必须轻量、快速、无依赖——它运行在JVM关闭线程中,不能依赖其他线程或未同步的资源
- 不要在Hook里调用
System.exit()或再次Runtime.getRuntime().halt(),会引发死锁或未定义行为 - 多个Hook按注册顺序反向执行,但无保证并行安全,避免共享可变状态
ShutdownHook里该清理哪些资源
核心原则:只清理当前JVM进程独占、且未被自动回收的资源。JVM停机时会自动释放堆内存、关闭未显式管理的文件描述符(靠GC和finalize机制),但以下必须手动处理:
- 打开的
ServerSocket或SocketChannel:不关闭会导致端口占用无法立即复用,K8s滚动更新失败 - 数据库连接池(如
HikariCP):需调用shutdown()而非仅close(),否则连接可能泄漏或事务中断 - 消息队列消费者(如
KafkaConsumer):调用close()确保offset提交完成,避免重复消费 - 定时任务调度器(如
ScheduledThreadPoolExecutor):调用shutdownNow()并awaitTermination(),防止任务中途丢弃
容易踩的坑:
– 把日志框架(如Logback)的LoggerContext.stop()放在Hook里,但某些版本存在竞态,建议改用ContextSelector生命周期管理
– 清理顺序错误:先关DB连接池,后关HTTP客户端,但HTTP客户端内部可能还依赖DB连接做审计日志
立即学习“Java免费学习笔记(深入)”;
为什么有时候ShutdownHook根本不执行
根本原因:JVM没机会走到关闭流程。最常见三种情况:
-
SIGKILL(kill -9):操作系统强制终止,绕过JVM所有钩子,无法拦截 - JVM崩溃(如
OutOfMemoryError触发的core dump)或内部致命错误,直接退出 - 主
Thread异常退出且未设置UncaughtExceptionHandler,而其他非守护线程仍在运行——JVM认为还有工作线程,不会启动关闭流程
验证方法:
– 在Hook开头加一行System.err.println("shutdown hook running"),配合strace -e trace=signal java MyApp观察是否收到SIGTERM
– 检查应用是否所有线程都设为守护线程(thread.setDaemon(true)),否则主程序退出后JVM仍挂起
Spring Boot应用要额外注意什么
Spring Boot 2.3+ 默认启用优雅停机(server.shutdown=graceful),但它和ShutdownHook是两层机制:前者处理Web容器(Tomcat/Jetty)的请求 draining,后者处理通用JVM资源。两者必须协同。
- Web容器停机期间,新请求会被拒绝,但已有请求会等待
spring.lifecycle.timeout-per-shutdown-phase(默认30s)再中断;此时ShutdownHook不应提前释放DB连接,否则请求报错 - 若使用
@PreDestroy,它比ShutdownHook更早执行,适合Bean级清理;但跨Bean依赖关系复杂时,不如统一收口到Hook里可控 - 配置
spring.main.allow-bean-definition-overriding=true可能导致自定义SmartLifecyclebean覆盖默认停机逻辑,需检查SmartLifecycle.stop()是否被正确调用
最容易被忽略的点:Kubernetes里livenessProbe在停机过程中持续失败,触发重启,导致ShutdownHook被反复打断——应缩短探针间隔或改用startupProbe兜底。










