1. Overview

In this tutorial, we're going to investigate the details of the JVM shutdown. Firstly we'll see in which conditions the JVM shuts down. Then we'll focus on the shutdown hooks and see how they're executed.

2. JVM Shutdown

The JVM shuts down either abruptly or normally. We'll first cover the orderly shutdown.

2.1. Normal Shutdown by Thread Count

When there remains no non-daemon thread, the JVM shuts down normally. We can test this by writing a small main function:

public static void main(String[] args) {
    System.out.println("Hello world!");
}

This code isn't multithreaded and runs on a single thread, the main thread. When it prints "Hello world!", the main thread terminates. As a result, the JVM starts the shutdown process since the only existing thread has terminated.

We'll next investigate the multi-threaded applications. When an application uses a thread pool, the pool manages the worker threads. So until the pool is terminated, the JVM doesn't shut down based on the thread count:

public void runWithPool() {
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    executorService.execute(() -> System.out.println("Hello world!"));
    
    System.out.println("Completing the method!");
}

In this example, we're defining a thread pool of size ten. Then we're executing a simple task printing "Hello world!". When the method completes, the JVM continues to run. This is because the thread pool contains a worker thread.

Another important point is that the thread pool only starts a core thread when we submit a task. For example, Executors.newFixedThreadPool(10) doesn't start ten threads right away. The pool reaches its desired core pool size after we submit ten tasks:

public void runWithPoolWithoutExecutingAnyTask() {
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    
    System.out.println("Completing the method!");
}

Unlike the previous example, the JVM shuts down after the method completes since the pool contains zero worker threads.

Keep in mind that worker threads contain an internal loop that keeps them alive. When we start a Thread manually, it terminates after the task completes:

public void runWithThread() {
    new Thread(() -> System.out.println("Hello world!")).start();
    
    System.out.println("Completing the method!");
}

Here, the runWithThread method runs on the main thread. The new thread starts and terminates in the current method. So when the main thread also terminates, the JVM shuts down.

So far we've used non-daemon threads. Next, we'll start a daemon thread to execute a task:

public void runWithDaemonThread() {
    Thread daemonThread = new Thread(new InfiniteRunner());
    daemonThread.setDaemon(true);
    daemonThread.start();
    
    System.out.println("Completing the method!");
}

Here, daemonThread runs a task that loops forever. But since it's a daemon thread, it doesn't prevent the JVM from terminating.

2.2. Normal Shutdown with System.exit

System.exit also initiates a normal shutdown.

public void exit() {
    new Thread(new InfiniteRunner()).start();

    System.out.println("Exiting main thread!");

    System.exit(0);
}

Here, when we invoke the System exit method, it terminates the JVM. Note that we're passing 0 as the exit status. By convention, non-zero status codes represent abnormal termination.

Alternatively, we can invoke Runtime.getRuntime().exit(0) instead of System.exit(0). They're effectively equivalent.

2.3. Normal Shutdown with CTRL-C

Pressing CTRL-C also initiates a normal shutdown. As we'll see in a moment, shutdown hooks enable us to capture the shutdown attempt and act on it.

2.4. Abrupt Shutdown with System.halt

We'll next terminate the JVM forcibly by invoking the System.halt method. This results in an abrupt shutdown where the JVM doesn't run the shutdown hooks or finalizers:

public void halt() {
    new Thread(new InfiniteRunner()).start();

    System.out.println("Halting main thread!");

    Runtime.getRuntime().halt(1);
}

In this method, after the halt invocation, the JVM terminates immediately.

3. JVM Shutdown Hooks

We'll now talk about the JVM shutdown hooks. The hooks are initialized but not-started Thread instances. The JVM starts these threads when a normal shutdown is in progress. They're mainly used to release the resources and do some cleanup.

3.1. Register Shutdown Hook

To register a shutdown hook, we must first create a thread. Then we must pass this thread to the Runtime.addShutdownHook method:

final Thread firstHook = new Thread(() -> System.out.println("First hook."));
Runtime.getRuntime().addShutdownHook(firstHook);

As the name implies, we can add multiple shutdown hooks.

3.2. Unregister Shutdown Hook

The hooks are registered by their object identities. So we can unregister a hook passing the same Thread instance to the removeShutdownHook method:

Runtime.getRuntime().removeShutdownHook(firstHook);

Note that we're using the same Thread instance that was used for registration.

4. When Do Hooks Run?

The shutdown hooks only run during a normal shutdown. This includes the cases:

  • when the last normal thread terminates
  • when someone invokes System.exit
  • when the Java process is interrupted - e.g. SIGINT

On a normal shutdown, the JVM starts all hook threads and they start running concurrently. We'll now register some hooks:

public void runHooksOnExit() {
    final Thread firstHook = new Thread(() -> System.out.println("First hook."));
    Runtime.getRuntime().addShutdownHook(firstHook);

    final Thread secondHook = new Thread(() -> System.out.println("Second hook."));
    Runtime.getRuntime().addShutdownHook(secondHook);

    System.out.println("Exiting...");
    System.exit(0); // Runtime.getRuntime().exit(status);
}

A sample run prints:

Exiting...
Second hook.
First hook.

An important note is that a hook mustn't depend on the execution order of others. If the execution order matters, a better option is merging all shutdown tasks into a single hook. This way we can guarantee the order of execution.

5. Exception Handling in Shutdown Hooks

Exception handling in a shutdown hook is similar to other threads. For example, we can register an UncaughtExceptionHandler instance to handle the uncaught exceptions:

public void exceptionHandlingInHooks() {
    final Thread hook = new Thread(() -> {
        throw new RuntimeException("Planned");
    });
    hook.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            System.out.println("Exception: " + e.getMessage());
        }
    });
    Runtime.getRuntime().addShutdownHook(hook);

    System.exit(0);
}

Here, we're registering the handler via the setUncaughtExceptionHandler method.

6. Application Threads and Thread Pools

Lastly, we'll investigate what happens to the application threads or thread pools during a shutdown. For example, we may have a crawler service with multiple threads backing it. Or we may have a ScheduledThreadPoolExecutor instance executing some scheduled tasks. During a shutdown, the JVM doesn't try to stop or notify these application threads. They continue to run alongside the hook threads. In the end, they just terminate abruptly.

7. Summary

In this tutorial, we've learned that the JVM can shut down either normally or abruptly. We also examined the usage of shutdown hooks. Lastly, we saw that the JVM doesn't attempt to stop application-owned threads.

As always the source code for all examples is available on Github.