How to Gracefully Shut Down a Spring Boot Application: Kill Signals, Hooks, and Thread Pools
This article explains the differences between kill -9 and kill -15 signals for Spring Boot, demonstrates how to use shutdown hooks, the Actuator endpoint, and proper thread‑pool termination to achieve an orderly application shutdown without data loss or resource leaks.
When managing Spring Boot applications, many operators use kill -9 <pid> to restart the embedded Tomcat, but this forceful approach bypasses graceful shutdown logic.
kill -9 and kill -15: what’s the difference?
Traditional WAR deployments are stopped via scripts, but Spring Boot packages the server with the application, raising the question of how to stop it cleanly. The kill command can send signals; kill -l lists them. Signal 9 (KILL) terminates a process immediately, while signal 15 (TERM) notifies the process to shut down voluntarily.
kill -l
HUP INT QUIT ILL TRAP ABRT BUS FPE KILL USR1 SEGV USR2 PIPE ALRM TERM STKFLT CHLD CONT STOP TSTP TTIN TTOU URG XCPU XFSZ VTALRM PROF WINCH POLL PWR SYSThe article provides a simple Spring Boot example with a DisposableBean implementation and a JVM shutdown hook that logs messages during shutdown.
@Component
public class TestDisposableBean implements DisposableBean {
@Override
public void destroy() throws Exception {
System.out.println("Test Bean destroyed ...");
}
}
@SpringBootApplication
@RestController
public class TestShutdownApplication {
public static void main(String[] args) {
SpringApplication.run(TestShutdownApplication.class, args);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("Executing ShutdownHook ...");
}));
}
}Testing steps:
Run java -jar test-shutdown-1.0.jar to start the application.
Issue kill -9 pid, kill -15 pid, or Ctrl+C and observe the logs.
Results show that kill -15 pid (or Ctrl+C) triggers the shutdown hook, logs the closing of the AnnotationConfigEmbeddedWebApplicationContext, executes the hook, and calls DisposableBean#destroy(). In contrast, kill -9 pid kills the process without any application logs.
Therefore, kill -15 pid allows the application to perform cleanup such as closing sockets, removing temporary files, notifying subscribers, and releasing resources, while kill -9 pid simulates a sudden crash.
How Spring Boot Handles the TERM Signal
When the JVM receives the TERM signal, the registered shutdown hook invokes ApplicationContext#close(), which calls doClose(). This method publishes a ContextClosedEvent, stops lifecycle beans, destroys cached singletons, closes the bean factory, and finally shuts down the embedded Tomcat.
@Override
public void registerShutdownHook() {
if (this.shutdownHook == null) {
this.shutdownHook = new Thread() {
@Override
public void run() {
synchronized (startupShutdownMonitor) {
doClose();
}
}
};
Runtime.getRuntime().addShutdownHook(this.shutdownHook);
}
}
@Override
public void close() {
synchronized (this.startupShutdownMonitor) {
doClose();
if (this.shutdownHook != null) {
Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
}
}
}Understanding this flow highlights why graceful shutdown is essential.
Other Graceful Shutdown Options
Spring Boot Actuator provides a /shutdown endpoint. Adding the dependency and enabling the endpoint allows a POST request to trigger a graceful shutdown.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
# Enable shutdown
endpoints.shutdown.enabled=true
# Disable authentication (use with caution)
endpoints.shutdown.sensitive=falseCalling curl -X POST host:port/shutdown returns {"message":"Shutting down, bye..."}. In production, secure this endpoint with Spring Security.
Gracefully Destroying Thread Pools
Thread pools must be shut down during application termination. Implementing DisposableBean allows explicit shutdown:
@Service
public class SomeService implements DisposableBean {
ExecutorService executorService = Executors.newFixedThreadPool(10);
public void concurrentExecute() {
executorService.execute(() -> System.out.println("executed..."));
}
@Override
public void destroy() throws Exception {
executorService.shutdownNow(); // or shutdown() with awaitTermination
}
}Understanding the difference between shutdown() (orderly) and shutdownNow() (attempts immediate stop) is crucial. Both require awaitTermination to ensure tasks finish or are cancelled.
/**
* Perform a shutdown on the underlying ExecutorService.
*/
public void shutdown() {
if (this.waitForTasksToCompleteOnShutdown) {
this.executor.shutdown();
} else {
this.executor.shutdownNow();
}
awaitTerminationIfNecessary();
}
private void awaitTerminationIfNecessary() {
if (this.awaitTerminationSeconds > 0) {
try {
this.executor.awaitTermination(this.awaitTerminationSeconds, TimeUnit.SECONDS);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}Additional strategies for graceful shutdown include traffic draining, using ACID transactions, ACK mechanisms in messaging, and idempotent designs for scheduled tasks.
When the virtual machine begins its shutdown sequence it will start all registered shutdown hooks in some unspecified order and let them run concurrently. When all the hooks have finished it will then run all uninvoked finalizers if finalization‑on‑exit has been enabled. Finally, the virtual machine will halt.
Shutdown hooks keep the JVM alive until they complete, allowing blocked operations to finish before the process exits.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
