Back to Daemon Threads & JVM Shutdown
Q2Question 2 of 3

Why does JVM not exit after main()?

Key Concepts
  • The JVM stays alive as long as any non-daemon thread is running, even after main() returns.
  • ExecutorService worker threads are non-daemon by default and keep the JVM alive indefinitely.
  • Forgetting to call pool.shutdown() causes the process to hang forever after all work is done.
  • Clean shutdown pattern: shutdown() then awaitTermination() then shutdownNow() as fallback.
  • Use Runtime.addShutdownHook() for emergency cleanup on SIGTERM, SIGINT, and System.exit().
  • A daemon ThreadFactory solves the problem but risks killing critical in-flight tasks on JVM exit.

Definition

The JVM stays alive as long as any non-daemon thread is running, so forgetting to call shutdown() on a thread pool causes the process to hang forever even after main() returns.
  • 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 on queue.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), then awaitTermination(), then shutdownNow() as fallback.
  • Shutdown hook – Runtime.addShutdownHook() registers cleanup code that runs on SIGTERM, SIGINT, and System.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.

JAVA
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!
    }
}
TEXT
Trade settled
main() finished!
(process hangs indefinitely)

How do you fix the problem?

Always call pool.shutdown() when you are done submitting tasks.

JAVA
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");
    }
}
TEXT
Trade settled
JVM exits cleanly

What is the production shutdown pattern?

The standard pattern handles both graceful and forced shutdown scenarios.

JAVA
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:

  • ExecutorService threads (Executors.newFixedThreadPool, etc.)
  • ScheduledExecutorService threads
  • java.util.Timer thread
  • 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.

JAVA
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 (unexecuted) |

What about try-with-resources for ExecutorService?

Since Java 19, ExecutorService implements AutoCloseable. The close() method calls shutdown() and awaitTermination() automatically.

JAVA
// 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

DIAGRAM
  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.

---

Share
Use ← → arrow keys to navigate