Understanding JVM Exit Mechanisms and Graceful Shutdown in Spring Boot Applications
This article explains the three JVM termination modes, how to use shutdown hooks and Spring's lifecycle events to implement graceful shutdown of HTTP, RPC and MQ entry points, manage thread‑pool termination, and avoid resource‑leak or inconsistent state during service redeployment.
Warm Tip
Over 400 Java interview questions are summarized at the end of the article; feel free to practice.
When a service is rolled out, how can we ensure that requests being processed or pending are handled correctly without business exceptions? A recent production incident involving Spring Event occurred because, during JVM shutdown, a thread tried to obtain a bean via Spring GetBean , but Spring forbids bean retrieval during BeanFactory destruction, leading to request failures and data inconsistency.
Why does Spring still allow bean retrieval after the container is destroyed? This is related to the shutdown order. To solve the problem, we first need to understand the JVM exit mechanisms.
Understanding JVM graceful exit ensures smooth service releases in production environments.
1. JVM Exit Methods
Java Virtual Machine has three exit methods: normal exit, abnormal exit, and forced termination.
Normal Shutdown
During a normal exit, the JVM executes shutdown hook callbacks. Frameworks like Spring register a shutdownHook to detect the exit signal and close the Spring context.
Four ways to trigger a normal shutdown:
All non‑daemon threads finish.
Calling System.exit(0) .
Pressing Ctrl+C in the console.
Sending a kill signal to the process (e.g., kill PID ).
Non‑daemon threads are those created by the application; daemon threads include JVM internal threads such as garbage collection. A thread can be turned into a daemon via Thread.setDaemon .
The following code demonstrates that the process does not exit until the secondary non‑daemon thread finishes:
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int cnt = 4;
for (int i = 0; i < cnt; i++) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println("被中断");
break;
}
System.out.println("子线程执行中");
}
System.out.println("子线程退出");
}
});
thread.start();
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程提出");Note: Do not use IDEA to run JUnit tests for this example because IDEA calls System.exit() after the test, preventing the child thread from completing.
In production, services may have thousands of threads; forcing all of them to exit via normal shutdown is difficult, so developers often use System.exit() or kill -9 to terminate the process.
Forced Termination
When the JVM is forcibly terminated, shutdown hook callbacks are not executed, resources cannot be released gracefully, and ongoing requests may be aborted.
Kill -9 the process.
Call Runtime.halt() .
Power loss or OS shutdown.
OS forcibly kills the process.
Process crash.
Forced termination is similar to a power cut; the process cannot run any cleanup code.
Operating systems may also kill a Java process when memory is exhausted. For example, a Docker container with 8 GB memory where the Java heap grows to 6 GB can be killed by the Linux kernel when the total memory usage approaches the host limit.
Abnormal Shutdown (OOM)
When a thread requests memory and the JVM cannot satisfy it, a Full GC is triggered. If memory is still insufficient, an OutOfMemoryError (OOM) is thrown. An OOM in a single thread does not crash the whole JVM, but it indicates a memory‑leak problem that usually warrants process termination.
JVM provides options to handle OOM:
-XX:+HeapDumpOnOutOfMemoryError – generate a heap dump. -XX:OnOutOfMemoryError – execute a command when OOM occurs. -XX:+ExitOnOutOfMemoryError – force the JVM to exit on OOM. -XX:+CrashOnOutOfMemoryError – exit and generate a crash log.
Example of using -XX:OnOutOfMemoryError to kill the process after a delay:
-XX:OnOutOfMemoryError="kill -15 %p && sleep 60 && kill -9 %p &"2. ShutdownHook Implementation
When the JVM receives a kill signal, registered shutdown hooks are executed. Example of adding a shutdown hook:
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
System.out.println("shutdown hook started");
}
}));The JDK documentation states that all hooks run concurrently in an undefined order; they must be thread‑safe and finish quickly. Calling System.exit() inside a hook blocks the shutdown process, and invoking System.exit() again can cause the JVM to hang.
Spring Boot already registers a shutdown hook, so developers should use Spring’s own lifecycle callbacks instead of adding another hook.
3. Spring’s Shutdown Extension Points
Spring’s shutdown flow (simplified):
protected void doClose() {
if (this.active.get() && this.closed.compareAndSet(false, true)) {
// ...
publishEvent(new ContextClosedEvent(this));
this.lifecycleProcessor.onClose();
destroyBeans();
closeBeanFactory();
onClose();
this.active.set(false);
}
}Publish ContextClosedEvent – listeners can stop accepting new traffic.
Invoke stop() on all beans implementing Lifecycle .
Destroy singleton beans (calling destroy‑method if defined).
Bean destruction order matters: the most recently created singleton is destroyed first. Using @DependsOn can enforce a specific order.
Example problem: a Kafka consumer bean may still try to send messages after the producer bean has been destroyed, causing data inconsistency.
4. Closing Entry Points
MQ
Consumers provide close or destroy methods. In a ContextCloseEvent listener, shut down all consumers and ensure the close method is idempotent.
RPC
When shutting down, deregister the service from the registry to stop receiving new RPC calls.
HTTP
Spring Boot’s embedded Tomcat is closed before the BeanFactory is destroyed, so the HTTP connector is shut down safely.
Graceful shutdown can be enabled via configuration:
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s5. Graceful Thread‑Pool Shutdown
Two ways to stop a thread pool:
shutdown – rejects new tasks, lets queued and running tasks finish.
shutdownNow – rejects new tasks, cancels queued tasks, attempts to interrupt running tasks.
Prefer shutdown for graceful termination; if tasks take too long, use awaitTermination with a timeout, then fall back to shutdownNow if necessary.
6. Summary
JVM has three exit methods: normal, OOM‑triggered, and forced.
Only normal exit and OOM configurations that run OnOutOfMemoryError scripts can perform graceful shutdown.
Graceful shutdown runs shutdown hooks concurrently; keep them short and thread‑safe.
Spring integrates shutdown hooks; use ContextClosedEvent to cut off HTTP/MQ/RPC traffic and then shut down thread pools.
Always ensure dependent resources or beans are not destroyed before they are no longer needed, otherwise unknown exceptions will occur during shutdown.
References
[1] https://www.bilibili.com/read/cv17442601/
[2] https://www.cnblogs.com/east4ming/p/17034195.html
[3] https://www.leyeah.com/article/spring-boot-elegant-shutdown-principle-detailed-699369
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
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.