Definition
- JVM exit rule – the JVM terminates only when all non-daemon threads have finished, regardless of whether
main()has returned. - Thread pool workers –
Executors.newFixedThreadPool()creates non-daemon worker threads by default, and they block onqueue.take()waiting for tasks forever. - Common culprits –
ExecutorService,ScheduledExecutorService,java.util.Timer, AWT Event Dispatch Thread, and any user-created non-daemon thread. - Clean shutdown – call
pool.shutdown()(stop accepting, finish queued), thenawaitTermination(), thenshutdownNow()as fallback. - Shutdown hook –
Runtime.addShutdownHook()registers cleanup code that runs on SIGTERM, SIGINT, andSystem.exit()but not on SIGKILL or JVM crash.
At National Bank of India, main() returning is like the branch manager going home. But the building stays open as long as any teller (non-daemon thread) is still at their counter. If 10 tellers are hired (thread pool) and nobody tells them to go home (shutdown()), they stand at their counters forever. The building lights never turn off. The fix: the manager's last action before leaving is "Everyone, go home!" (pool.shutdown()). Wait for all tellers to finish their current customers. Lock the building. JVM exits.
Understanding the Concepts
Why does the JVM not exit after main()?
The JVM does not exit when main() returns. It exits when all non-daemon threads have terminated. Thread pool workers created by Executors.newFixedThreadPool() are non-daemon by default. They sit in a loop calling queue.take(), blocking forever waiting for tasks that never come.
package com.bank.service;
import java.util.concurrent.*;
public class JvmHangDemo {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(() -> System.out.println("Trade settled"));
System.out.println("main() finished!");
// main() returns but JVM does NOT exit!
// 10 pool threads are non-daemon, alive, waiting for tasks.
// Process hangs forever. Must kill with kill -9!
}
}
Trade settled
main() finished!
(process hangs indefinitely)
How do you fix the problem?
Always call pool.shutdown() when you are done submitting tasks.
package com.bank.service;
import java.util.concurrent.*;
public class CleanShutdownDemo {
public static void main(String[] args) throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(10);
try {
pool.submit(() -> System.out.println("Trade settled"));
} finally {
pool.shutdown(); // stop accepting, finish queued tasks
boolean done = pool.awaitTermination(30, TimeUnit.SECONDS);
if (!done) {
pool.shutdownNow(); // force stop
}
}
System.out.println("JVM exits cleanly");
}
}
Trade settled
JVM exits cleanly
What is the production shutdown pattern?
The standard pattern handles both graceful and forced shutdown scenarios.
package com.bank.service;
import java.util.concurrent.*;
public class ProductionShutdown {
private static final ExecutorService pool =
Executors.newFixedThreadPool(10);
static {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("Shutdown hook: stopping pool...");
pool.shutdown();
try {
if (!pool.awaitTermination(30, TimeUnit.SECONDS)) {
System.out.println("Forcing shutdown...");
pool.shutdownNow();
pool.awaitTermination(10, TimeUnit.SECONDS);
}
} catch (InterruptedException e) {
pool.shutdownNow();
Thread.currentThread().interrupt();
}
System.out.println("Pool terminated");
}));
}
public static void main(String[] args) {
pool.submit(() -> System.out.println("Processing payment"));
// Shutdown hook runs on SIGTERM (kill), SIGINT (Ctrl+C)
}
}
What are the common non-daemon threads that prevent exit?
Several Java APIs create non-daemon threads by default:
ExecutorServicethreads (Executors.newFixedThreadPool, etc.)ScheduledExecutorServicethreadsjava.util.Timerthread- AWT/Swing Event Dispatch Thread
- RMI and JMX threads
- Any Thread created without
setDaemon(true)
Can you use a daemon ThreadFactory instead?
Yes, but with a trade-off. Daemon pool threads let the JVM exit without calling shutdown(), but in-flight tasks may be killed mid-execution.
ExecutorService daemonPool = Executors.newFixedThreadPool(10, r -> {
Thread t = new Thread(r);
t.setDaemon(true); // won't prevent JVM exit
return t;
});
// Good for: optional background tasks (cache warming, metrics)
// Bad for: critical tasks (payments, settlements)
How is shutdown() different from shutdownNow()?
| Feature | shutdown() | shutdownNow() |
|---|---|---|
| New tasks | Rejected | Rejected |
| Running tasks | Finish normally | Interrupted |
| Queued tasks | Execute then stop | Returned as list, not executed |
| Return value | void | List
What about try-with-resources for ExecutorService?
Since Java 19, ExecutorService implements AutoCloseable. The close() method calls shutdown() and awaitTermination() automatically.
// Java 19+
try (ExecutorService pool = Executors.newFixedThreadPool(10)) {
pool.submit(() -> System.out.println("Trade settled"));
} // pool.close() called automatically: shutdown + await
What are common interview follow-up questions?
- Does shutdown() block? No, it returns immediately. Use awaitTermination() to block.
- Can you submit tasks after shutdown()? No, it throws RejectedExecutionException.
- When does the shutdown hook NOT run? On SIGKILL (kill -9), JVM crash, or Runtime.halt().
- How does Docker stop interact with shutdown hooks? Docker sends SIGTERM first, then SIGKILL after a timeout (default 10s).
Visual Overview
main() returns
│
▼
Are all non-daemon threads done?
│
┌───┴───┐
│ NO │ YES
▼ ▼
JVM stays JVM exits
alive! cleanly
│
▼
Pool threads still alive:
┌────────────────────────────────┐
│ Worker-1: queue.take() BLOCKS │
│ Worker-2: queue.take() BLOCKS │
│ ... │
│ Worker-10: queue.take() BLOCKS │
└────────────────────────────────┘
Process hangs forever!
Fix: pool.shutdown()
│
▼
Workers finish current tasks
│
▼
All workers done → JVM exits
Key Takeaway
The JVM does not exit when main() returns. It stays alive as long as any non-daemon thread is running. Thread pool workers created by Executors are non-daemon by default. They block on queue.take() waiting for tasks that never come, keeping the JVM alive indefinitely. This is a common production bug: the service finishes all work but the process appears "hung" because pool threads are still alive.
The clean shutdown pattern is shutdown() followed by awaitTermination() followed by shutdownNow() as a fallback. Always add a Runtime.addShutdownHook() as a safety net for SIGTERM and SIGINT. In Spring Boot, the framework handles shutdown for its managed executors, but custom pools are your responsibility.
At National Bank of India, a containerized settlement service forgot to call shutdown(). Docker sent SIGTERM and waited 30 seconds, then sent SIGKILL. Fifteen in-flight settlements were lost. Adding a shutdown hook with graceful pool termination fixed the problem: clean exit within 30 seconds, zero data loss.
Remember one sentence: non-daemon pool threads keep the JVM alive forever, so always call shutdown() when you are done.
---