Java进程必须是PID 1,否则收不到Docker stop发送的SIGTERM信号;Shell启动会使其占据PID 1且不转发信号,导致Java子进程无法响应,容器被强制SIGKILL终止。

Java进程必须是PID 1,否则收不到SIGTERM
Docker stop 只向容器内 PID 1 进程发 SIGTERM,而 Shell 启动脚本(如 sh -c "java -jar app.jar")会让 Shell 占据 PID 1,Java 变成子进程——Shell 默认不转发信号,Java 就完全收不到。现象是:执行 docker stop 后容器立刻被 SIGKILL 强杀,日志里看不到任何优雅关闭痕迹。
解决方式只有一条路:exec 替换当前 Shell 进程:
- 在启动脚本里写
exec java -jar app.jar,不是java -jar app.jar - 如果用
ENTRYPOINT ["sh", "-c", "java -jar app.jar"],必须改成ENTRYPOINT ["sh", "-c", "exec java -jar app.jar"] - Spring Boot 官方推荐的
java -Djava.security.egd=file:/dev/./urandom -jar app.jar同样要加exec前缀
用 Runtime.addShutdownHook 捕获可拦截信号
JVM 能响应 SIGTERM(即 kill -15),但不能响应 SIGKILL(kill -9)。addShutdownHook 是唯一可靠入口,它会在 JVM 收到终止信号后、退出前自动触发。
注意几个关键事实:
立即学习“Java免费学习笔记(深入)”;
-
@PreDestroy或SmartLifecycle.stop()不起作用——它们依赖 Spring 容器主动关闭,而docker stop是系统级信号,绕过 Spring 生命周期 - ShutdownHook 线程里可以做阻塞操作(比如等线程池清空),但不能依赖 Spring Bean 的完整上下文(部分 Bean 可能已销毁)
- 别在 Hook 里调
System.exit()——这会引发二次 shutdown,可能死锁
典型写法:
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
logger.info("Received SIGTERM, starting graceful shutdown");
webServer.shutdown(); // 如果是嵌入式 Web 容器
taskExecutor.shutdown();
try {
if (!taskExecutor.awaitTermination(30, TimeUnit.SECONDS)) {
taskExecutor.shutdownNow();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}));
jstack 导出堆栈必须靠 trap + exec 配合
想在 docker stop 时自动生成线程快照,不能依赖 Java 层信号处理——JVM 不会因 SIGTERM 自动调 jstack。必须用 Shell 层 trap 捕获信号,再显式调用 jstack。
前提条件缺一不可:
- Java 进程是 PID 1(否则
jstackattach 失败,报Unable to open socket file) - 容器镜像含
jstack(OpenJDK 镜像默认有,Alpine + JRE 镜像通常没有) - 没启用
-XX:+DisableAttachMechanism(该参数会彻底禁用jstackattach)
最小可行 entrypoint.sh:
#!/bin/sh set -e <h1>启动 Java 并记录 PID</h1><p>java -jar app.jar & JAVA_PID=$!</p><h1>捕获 TERM,导出堆栈后转发信号给 Java</h1><p>trap "jstack -l $JAVA_PID > /tmp/threaddump-$(date +%s).txt 2>&1; kill -TERM $JAVA_PID; wait $JAVA_PID" TERM</p><h1>等待 Java 退出</h1><p>wait $JAVA_PID
然后 Dockerfile 中写:ENTRYPOINT ["./entrypoint.sh"],且确保该脚本有执行权限。
别信“Spring Boot Actuator /actuator/shutdown”
这个 endpoint 默认是关闭的,开启后也只接受 HTTP 请求,和 docker stop 完全无关。它本质是让 Spring 主动调 context.close(),触发的是 Spring 生命周期回调,不是系统信号流程。生产环境若依赖它实现停机,等于把容器生命周期交给外部 HTTP 调用控制——既多一层故障点,又无法保证调用时机(比如 K8s preStop 超时了怎么办?)。
真正需要的是信号链路端到端可控:Docker → PID 1 → JVM ShutdownHook → 应用逻辑。中间插一个 HTTP 层,就破坏了这个确定性。
最容易被忽略的一点:Alpine 镜像里 jstack 不可用,exec 写错位置导致 PID 1 不是 Java,以及 DisableAttachMechanism 参数静默屏蔽所有诊断能力——这三个问题叠加,会让整个优雅停机方案看起来“完全没反应”。


















