Java进程必须是PID 1,因JVM需直接接收SIGTERM才能执行shutdown hook;CMD须用exec形式(JSON数组),避免shell不转发信号导致优雅停机失败。

OpenJDK 容器启动命令用 CMD 的 exec 形式(即 JSON 数组语法)不是“可选优化”,而是生产环境 Java 服务的刚性要求——它直接决定容器能否被 Kubernetes 正常驱逐、JVM 能否收到 SIGTERM 并执行 shutdown hook。
为什么 Java 进程必须是 PID 1?
Java 应用(如 Spring Boot 打包成的 jar)依赖 JVM 启动,而 JVM 本身需要响应操作系统信号才能触发 Runtime.addShutdownHook()。如果用 shell 模式写 CMD java -jar app.jar,实际 PID 1 是 /bin/sh,SIGTERM 先发给 shell;但默认的 sh 不会自动转发信号,JVM 根本收不到,导致强制 kill、连接未优雅关闭、事务中断等线上问题。
exec 形式让 JVM 进程直接成为 PID 1:CMD ["java", "-jar", "app.jar"],此时 SIGTERM 直达 JVM,shutdown hook 可正常运行。
exec 形式下怎么处理环境变量和 JVM 参数?
exec 模式不解析 $JAVA_HOME 或 $PROFILE,但你不需要退回到 shell 模式。正确做法是:
- 在
Dockerfile中用ENV预设变量,如ENV JAVA_HOME=/opt/java,这些变量对 exec 模式下的进程可见 - JVM 参数统一写进
CMD数组:例如CMD ["java", "-Xms512m", "-Xmx1g", "-jar", "app.jar"] - 若需动态拼接(如根据环境选配置文件),写一个轻量
entrypoint.sh,并在其中用exec "$@"调用 JVM,再配ENTRYPOINT ["./entrypoint.sh"]和CMD ["-jar", "app.jar"]
OpenJDK 镜像里 /bin/sh 不一定支持 signal forwarding
很多精简 OpenJDK 基础镜像(如 eclipse/openjdk:17-jre、amazoncorretto:17-jre-alpine)用的是 busybox sh 或 dash,它们默认不转发信号。即使你写 CMD java -jar app.jar(shell 模式),也大概率收不到 SIGTERM。这不是 bug,是设计使然——alpine 的 sh 就没实现 kill -TERM -- -$$ 这类转发逻辑。
exec 形式绕过了这个不可靠层,把信号控制权交还给 JVM 本身,兼容性反而更好。
别忽略 ENTRYPOINT 和 CMD 的组合陷阱
如果你用了 ENTRYPOINT ["java"],再写 CMD ["-jar", "app.jar"],最终执行的是 java -jar app.jar,这仍是 exec 模式,没问题。但注意:
-
ENTRYPOINT写成字符串形式(如ENTRYPOINT java)就退化为 shell 模式,PID 1 又变成 shell -
docker run后追加的命令会覆盖CMD,但不会覆盖ENTRYPOINT;所以确保ENTRYPOINT本身也是 exec 形式 - Kubernetes 的
lifecycle.preStop发送SIGTERM时,只作用于 PID 1 —— 这个细节一旦漏掉,滚动更新就会卡住
真正容易被忽略的点是:哪怕你本地 docker run -it 测试时一切正常,只要没在 Kubernetes 或 docker swarm 环境下模拟 SIGTERM 触发流程,就无法验证 shutdown hook 是否真能执行。建议在应用里加一行日志打印 Runtime.getRuntime().addShutdownHook(...),并用 docker stop -t 30 观察输出。


















