JVM Startup and Shutdown: Bootstrap, Initialization Hooks, and Shutdown Sequence

Understanding JVM lifecycle: bootstrap process, initialization hooks, shutdown sequence, and how to implement lifecycle callbacks for graceful shutdown.

published: reading time: 32 min read author: GeekWorkBench
Quick Summary

Understanding JVM lifecycle: bootstrap process, initialization hooks, shutdown sequence, and how to implement lifecycle callbacks for graceful shutdown.

JVM Startup and Shutdown: Bootstrap, Initialization Hooks, and Shutdown Sequence

The JVM goes through a well-defined lifecycle from startup to shutdown. Understanding this lifecycle helps you debug startup issues, implement graceful shutdown, and use initialization hooks for setup tasks that must run before your application starts.

This post covers the JVM bootstrap process, how classes are loaded during startup, initialization hooks that run on startup and shutdown, and the shutdown sequence itself.

Introduction

The JVM follows a rigorous lifecycle from the moment you invoke the java command until the process terminates. The bootstrap phase loads core JDK classes, initializes the garbage collector and JIT compiler, registers shutdown hooks, and eventually transfers control to your application main method. At the other end, the shutdown sequence runs registered hooks concurrently, finalizes remaining objects, and calls _exit() to hand control back to the operating system. Most developers never need to think about this lifecycle — it works correctly by default. But when startup problems surface in containerized environments, when graceful shutdown does not behave as expected, or when initialization hooks need to warm caches before the application begins, understanding the JVM lifecycle becomes essential.

For practitioners, JVM startup and shutdown matters in three scenarios. First, when debugging class loading order problems or circular dependency failures during bootstrap, where the sequence of events determines whether initialization succeeds or throws ExceptionInInitializerError. Second, when implementing graceful shutdown in services where containers send SIGTERM and expect the application to clean up and exit within a timeout — shutdown hooks that block indefinitely or get skipped entirely cause crashes, failed liveness probes, or resource leaks. Third, when working with initialization hooks for pre-warming caches or setting up profiling agents like JVMTI tools that must run before application code executes. Understanding when shutdown hooks run, what can cause them to be skipped, and how signal handling interacts with the lifecycle prevents production incidents that are difficult to debug after the fact.

This post walks through the bootstrap sequence step by step, explains how initialization and shutdown hooks work and what they can and cannot do, covers the five phases of the JVM shutdown sequence, describes signal handling on Unix-like systems, and provides production failure scenarios and implementation patterns for graceful shutdown. By the end, you will understand exactly what happens between java -jar and your application starting, and between calling System.exit() and the process actually terminating.

When NOT to Use

JVM startup and shutdown details matter when debugging startup failures, implementing graceful shutdown, or integrating with container orchestrators. A simple application that exits cleanly does not need you to understand bootstrap sequencing or hook registration. The JVM handles these correctly for most cases without intervention. Only dig into lifecycle internals when you hit class loading order problems, hook timing issues, or signal handling bugs.

Do not use initialization hooks for setup that belongs in static initializers or your main method. Hooks are JVM-internal and harder to debug. Do not use shutdown hooks for cleanup that must run every time, either. Shutdown hooks can be skipped entirely if the process gets killed with SIGKILL, the JVM crashes, or someone calls halt(). Put critical cleanup synchronously during normal operation, not asynchronously at shutdown where it may never execute.

Modifying the bootstrap classpath is rarely necessary and frequently breaks JVM assumptions. If you are adding classes there to work around classloader issues, fix the classloader problem directly. Shutdown hook timing is not guaranteed relative to specific application threads; do not use hooks for operations that need ordering guarantees without explicit synchronization. Runtime Data Areas and Execution Engine cover what gets initialized during bootstrap.

JVM Bootstrap Process

When you run java -jar myapp.jar, the JVM begins a complex bootstrap sequence before your main method executes.

Bootstrap Sequence

sequenceDiagram
    participant OS as Operating System
    participant JVM as JVM Process
    participant Boot as Bootstrap Loader
    participant Main as Main Thread

    OS->>JVM: Create process, load JVM library
    JVM->>Boot: Initialize Bootstrap ClassLoader
    Boot->>Boot: Load core JDK classes
    JVM->>JVM: Create runtime system (Thread, GC, etc.)
    JVM->>JVM: Register shutdown hooks
    JVM->>Main: Load application main class
    Main->>Main: Execute main() method
    Note over Main: Application runs
    Main-->>JVM: main() returns or System.exit()
    JVM->>JVM: Run shutdown hooks
    JVM->>JVM: Finalize finalize() methods
    JVM-->>OS: Process exits

Step-by-Step Bootstrap

  1. Operating System creates the JVM process. This involves loading the JVM dynamic library (libjvm.so on Linux, jvm.dll on Windows) into memory and initializing native structures.

  2. Bootstrap ClassLoader loads. The native bootstrap loader loads core Java classes from rt.jar (Java 8 and earlier) or from the modules image (Java 9+). These include java.lang.Object, java.lang.Class, java.lang.String, and all other fundamental classes.

  3. JVM runtime system initializes. This includes:

    • Creating the GC system
    • Initializing the interpreter
    • Setting up the JIT compiler infrastructure
    • Creating the main thread and thread group
    • Initializing the class loader subsystem
  4. Command line arguments are processed. The JVM parses -X, -XX, and system properties. These affect GC algorithms, heap sizing, JIT behavior, and more.

  5. Shutdown hook registration. The JVM registers internal shutdown hooks (for flight recorder, etc.) and makes the application hooks registered via Runtime.addShutdownHook() available.

  6. Application main class loads. The class specified on the command line is loaded. Static initializers run.

  7. Main method executes. Control transfers to your application code.

Initialization Hooks

The JVM supports initialization hooks that run during startup. These are not the same as shutdown hooks, though the API is similar.

Using Initialization Hooks

Initialization hooks are threads that start during JVM initialization, before the application starts. They are useful for:

  • Pre-warming caches
  • Initializing native libraries
  • Verifying external dependencies
  • Setting up JVMTI (JVM Tool Interface) agents
public class InitializationHookExample {
    static {
        System.out.println("Static initializer runs during class loading");
    }

    public static void main(String[] args) {
        System.out.println("Main method runs after initialization hooks");
    }
}

Pre-Initialization with -XX:CompileCommand

For extremely early initialization, you can use the CompileCommand to force JIT compilation before first use:

java -XX:CompileCommand=option,MyClass::hotMethod,BiasedLocking -jar myapp.jar

This compiles MyClass.hotMethod() with biased locking enabled before the application starts.

Shutdown Hooks

Shutdown hooks are threads that the JVM runs during the shutdown sequence. They are guaranteed to run even if the JVM terminates abruptly.

When Shutdown Hooks Run

Shutdown hooks run when:

  • All non-daemon threads have finished
  • System.exit() is called (from application code or another shutdown hook)
  • The JVM receives a termination signal (SIGTERM, Ctrl+C) and has not disabled shutdown hooks
  • Runtime.halt() is called (forceful termination without running hooks, but shutdown hooks ARE run unless forced with _exit)

Implementing Shutdown Hooks

import java.util.concurrent.*

public class ShutdownHookExample {
    private static final ExecutorService executor =
        Executors.newFixedThreadPool(4);

    public static void main(String[] args) {
        // Register shutdown hook
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("Shutdown hook running");
            shutdownExecutor();
        }, "shutdown-hook"));

        // Application code
        System.out.println("Application running");
    }

    private static void shutdownExecutor() {
        executor.shutdown();
        try {
            if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }
}

Shutdown Hook Rules

  1. Do not reference daemon threads. Daemon threads may terminate before the shutdown hook completes.

  2. Do not depend on other application threads. By the time shutdown hooks run, all non-daemon threads have terminated.

  3. Keep shutdown hooks fast. The JVM waits for all shutdown hooks to complete before exiting. Slow shutdown hooks delay termination.

  4. Do not use System.exit() in a shutdown hook. This causes a deadlock as the JVM waits for the current shutdown hook to complete before starting the new exit.

  5. Handle exceptions. Uncaught exceptions in shutdown hooks terminate the hook but do not stop other shutdown hooks.

  6. Do not assume hooks run in a particular order. Multiple hooks may run concurrently.

Shutdown Sequence

The JVM shutdown sequence follows a defined order:

Phase 1: Shutdown Request

The JVM does not proactively monitor for termination — it transitions to the shutdown sequence only after a specific trigger fires. The primary trigger is the last non-daemon thread completing, which happens when main() returns and all spawned non-daemon threads have exited. At that point the JVM checks whether shutdown hooks exist, and if so, begins the shutdown sequence. Calling System.exit() directly forces the transition immediately regardless of thread state. Signals like SIGTERM or SIGINT sent from the operating system are received by the JVM process and translated into an internal shutdown request; the JVM sets a flag and defers actual shutdown until a safe point, so hooks do not run the instant the signal arrives but at the next safepoint where the JVM can safely pause application threads. Runtime.halt() bypasses this entire machinery and terminates the process on the spot, which is why it is treated separately from graceful shutdown.

What trips up developers in practice is confusing “last non-daemon thread terminating” with “signal received.” In a server application with a long-running thread pool, the JVM never naturally reaches the shutdown sequence because those threads never exit. That is why frameworks call System.exit() explicitly from their own shutdown method — they need to force the transition rather than wait indefinitely. In containerized environments, the orchestrator sends SIGTERM and expects the process to exit within a timeout; if the JVM is not in a state where it can process that signal, it defers until a safepoint, which means the container can get SIGKilled before hooks ever run.

Phase 2: Running Shutdown Hooks

The JVM uses its internal VMThread to coordinate the shutdown sequence, so hooks do not run on application threads. When the JVM decides to proceed with shutdown, it creates a new thread for each registered hook — these threads are not daemon threads, so they prevent the JVM from exiting prematurely even if they take time. Hooks run concurrently, not sequentially: two hooks can be executing at the same time on different threads. This has consequences. Hooks must not assume any particular running order, and two hooks that try to acquire the same lock in opposite directions risk deadlock.

The JVM waits for every hook to complete before moving to the next phase. There is no timeout. If a hook hangs, the JVM hangs. “Keep shutdown hooks fast” is not just a performance guideline — it is a liveness guarantee. A hook that blocks on network I/O with no timeout, waits on a latch that never releases, or enters an infinite loop freezes the entire shutdown sequence. Container orchestrators watching for process exit eventually SIGKILL the process after their timeout expires, which means slow hooks result in SIGKILL regardless of the JVM’s intent to run them cleanly.

If a hook calls Runtime.halt(), the JVM terminates immediately and abandons all other hooks that have not yet started or finished. This is the one mechanism that breaks the “all hooks run” guarantee. If a hook calls System.exit(), the JVM deadlocks — it is already waiting for the hook to complete, and System.exit() attempts to start a new shutdown sequence, which cannot proceed while the first one is in progress.

Phase 3: Finalization

Once every shutdown hook finishes, the JVM moves to the finalization phase. It maintains a finalizer queue — objects with a finalize() method that have been detected as unreachable are added to this queue. The JVM drains the queue by calling finalize() on each object, one at a time, in a single dedicated thread. This thread runs at low priority and can be throttled by the garbage collector when there are many objects to finalize. If a finalizer resurrects an object by making it reachable again, the object escapes the queue and subsequent finalizers are not called on it.

Finalization is disabled by default in most JVM configurations, and can also be disabled explicitly with -XX:+DisableExplicitGC. When disabled, the finalizer queue is never drained and no finalize() methods run. Object.finalize() is deprecated since Java 9 — its guarantees are weak, its ordering is undefined, it can resurrect objects unpredictably, and it adds measurable overhead to every object allocation. Production systems that rely on finalizers for cleanup are almost always broken: finalizers can run in any order, or not at all if the JVM exits before the queue drains.

For resource cleanup, use java.lang.ref.Cleaner instead, introduced in Java 9 as the replacement. A Cleaner registers an action that runs when the object becomes unreachable, but unlike finalizers, the action runs in a dedicated thread with defined ordering and no resurrection semantics. For file handles and connections, try-with-resources is the most reliable pattern — it guarantees cleanup at a specific point in your code regardless of exceptions or shutdown state. Both Cleaner and finalizers are suppressed if the JVM crashes or is killed; nothing runs after _exit() has been called.

Phase 4: Process Exit

The JVM calls _exit() to terminate the process. At this point, all shutdown hooks have completed and finalization is done. The _exit() function is a POSIX system call that hands control back to the operating system, but before that happens the JVM performs a few last operations. Native resources held by the JVM — memory mapped files, JNI references, thread-local storage — are released through the native finalization chain. The JVM’s embedded JDK classes (the ones loaded by the bootstrap loader) are unloaded, which triggers their native finalizers. The module system, if used, unwinds the module graph. All of this happens before _exit() is called, which means by the time the process reaches that call the JVM heap and native memory are already clean.

The _exit() call itself takes an integer return code. By convention, a return code of zero means normal termination and any non-zero value indicates an error condition. The JVM sets this code based on how shutdown was initiated: zero for normal System.exit() calls, non-zero when the JVM aborts due to an uncaught exception or internal error. If a shutdown hook calls System.exit(), the return code from that call is what gets passed to the OS. Container orchestrators and service managers use this return code to determine whether the process exited cleanly or crashed; a non-zero code typically triggers an alert or restart policy. You can observe this in Kubernetes liveness probes, systemd service configurations, or CI pipelines that check $? after running a Java process.

After _exit() returns, the operating system performs its own cleanup. The kernel reclaims the process’s file descriptors, releases memory mapped regions, and removes the process from the system task table. At this point nothing remains of the JVM — no zombie process, no dangling threads, no held ports. The process table entry is updated with the return code so parent processes or monitoring tools can retrieve it.

The entire journey from the first java command to _exit() is invisible to the Java developer, but understanding what happens at each phase matters when something goes wrong. A process that hangs on shutdown is usually waiting in Phase 2 for a slow hook. A process that exits with the wrong code set a non-zero return value somewhere in its shutdown sequence. A container that gets SIGKilled never reached Phase 4 at all. These failure modes become straightforward to debug once the shutdown sequence is no longer a black box.

Signal Handling

On Unix-like systems, the JVM handles signals for:

  • SIGTERM: Graceful shutdown request. Runs shutdown hooks.
  • SIGINT: Interrupt from Ctrl+C. Runs shutdown hooks.
  • SIGQUIT: Thread dump request (jstack). Does not trigger shutdown.
  • SIGABRT: JVM crash. Generates hs_err crash log.
  • SIGHUP: Often used for configuration reload. Not handled by JVM by default.
# Send SIGTERM for graceful shutdown
kill -TERM <pid>

# Send SIGINT (Ctrl+C equivalent)
kill -INT <pid>

# Request thread dump without killing
kill -QUIT <pid>

Custom Signal Handling

JVM TI (Tool Interface) agents can register for signal handling:

// JVM TI agent registration
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved) {
    jvmtiEnv *jvmti;
    (*vm)->GetEnv(vm, (void **)&jvmti, JVMTI_VERSION_1_0);

    // Set notification for breakpoint, single step, etc.
    jvmtiError err = (*jvmti)->SetEventNotificationMode(
        jvmti, JVMTI_ENABLE,
        JVMTI_EVENT_BREAKPOINT, NULL);

    return JNI_OK;
}

Production Failure Scenarios

ScenarioRoot CauseSymptomsResolution
Shutdown hooks do not runJVM killed with SIGKILLCleanup code skippedAvoid SIGKILL, use SIGTERM
Deadlock during shutdownHook calls System.exit()JVM hangs on shutdownRemove System.exit() from hooks
Slow shutdown hooksBlocking I/O in hookDelayed shutdownMake hooks non-blocking
Shutdown hook exceptionUncaught exception in hookHook terminates, others continueAlways catch exceptions in hooks
Finalizers run unexpectedlyObject became unreachableDelayed cleanup, resource leaksAvoid finalize(), use Cleaner
Signal handling blockedLong shutdown hook or GCSIGTERM ignoredKeep hooks fast, tune GC

Trade-off Analysis

Trade-offConsiderations
Fast Shutdown vs. Clean ShutdownForcing immediate termination risks data loss. Waiting for cleanup takes time.
Daemon vs. Non-Daemon ThreadsDaemon threads die with the JVM, potentially leaving cleanup incomplete. Non-daemon threads keep the JVM alive.
Synchronous vs. Asynchronous CleanupSynchronous cleanup is simpler but can block. Asynchronous cleanup is non-blocking but more complex.
Signal Handling vs. Shutdown HooksSignals provide immediate response. Shutdown hooks run after the decision to terminate is made.

Failure Scenarios Deep Dive

JVM Fails to Start Due to Classpath Issues

A static initializer in class A triggers loading of class B, whose static initializer triggers loading of class C, which eventually triggers loading back to class A before A’s static initialization completes. The JVM detects this circularity and throws a StackOverflowError or ExceptionInInitializerError. This happens when static fields have complex initialization expressions that indirectly reference the same class. The fix is to break the circular dependency: move the initialization to a separate method called explicitly after construction, or refactor to remove the circular reference.

During bootstrap, the JVM loads core JDK classes first, then application classes. Static initializers run at class loading time, so if ClassA’s static field initializer triggers a method that loads ClassB, ClassB’s initializer loads ClassC, and ClassC circles back to ClassA, the JVM catches the cycle and fails. ExceptionInInitializerError wraps the original exception, usually a StackOverflowError from deep recursion or a linkage error from a verification failure. The class stays in a failed state; any thread that tries to use it gets the same error.

This shows up when static fields meet service loaders, SPI implementations, or dependency injection frameworks that resolve eagerly at class load time. A logger field that initializes a static service locator, a configuration class that builds a singleton in a static block, or a registry that registers itself during load, any of these can trigger a cycle under the right import structure.

The fix is to defer initialization. Use lazy holder patterns, static factories that postpone to a separate method, or constructor-based injection instead of static field initialization. Resolve the dependency at first use inside a synchronized getter, or refactor so the dependency gets passed in rather than pulled in statically.

Shutdown Hook Never Runs Because of Security Manager

If your application uses non-daemon threads that never terminate (for example, a thread pool with blocking tasks, or a long-running background task), the JVM never proceeds to the shutdown hook phase. The application just sits running even after main() returns. Ensure your application explicitly calls System.exit() or sets daemon threads appropriately if you rely on shutdown hooks for cleanup.

The JVM waits for all non-daemon threads to finish before entering the shutdown sequence. A non-daemon thread keeps the JVM alive even after main() returns. This surprises a lot of developers: a thread pool with default settings (non-daemon threads), a scheduled task that runs indefinitely, a blocking queue consumer waiting on an empty queue, all of these prevent shutdown hooks from ever running. The JVM just sits there running, waiting for threads that are not going to terminate on their own.

Daemon threads work differently. The JVM proceeds to the shutdown hook phase when only daemon threads remain or when all non-daemon threads have terminated. Daemon threads get stopped abruptly when the JVM proceeds to shutdown; they do not run cleanup code. If you want a thread to not block shutdown but still run during normal operation, mark it as a daemon thread. Set Thread.setDaemon(true) before starting the thread, or configure thread pools with a ThreadFactory that creates daemon threads.

If your application has non-daemon threads by design (a long-running background worker, a thread pool handling blocking I/O), and you want shutdown hooks to run, you have two choices. Call System.exit() explicitly once your work is done; do not rely on all threads terminating naturally. Or mark threads that do not need to block shutdown as daemon threads using a custom ThreadFactory. For an ExecutorService, build a factory that sets daemon=true on every thread it creates, and pass that factory when building the pool.

The shutdown hook phase only starts once the JVM decides to terminate, not when main() returns. If non-daemon threads are still running, blocked, or waiting on I/O, the JVM does not proceed to the hook phase. Only when those threads have all terminated does the JVM transition to running shutdown hooks. This matters in containerized environments where orchestrators send SIGTERM and expect the application to shut down within a timeout. If your non-daemon threads are blocking, the JVM never reaches the shutdown hooks, and the container gets a SIGKILL.

Flight Recorder Crash Dump Generation Failure

When the JVM crashes (SIGABRT), it generates an hs_err log and may start a Java Flight Recorder dump. If the disk fills up during the dump or the process is killed immediately after generating the crash log, the dump may be incomplete or truncated. Configure -XX:ErrorFile= to write the hs_err log to a partition with sufficient space, and ensure monitoring detects disk space issues before they cause dump truncation.

Implementation Patterns

// Graceful shutdown with proper resource cleanup
public class GracefulShutdown {
    private volatile boolean shuttingDown = false;

    public static void main(String[] args) {
        GracefulShutdown app = new GracefulShutdown();
        app.registerShutdownHook();
        app.run();
    }

    private void registerShutdownHook() {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            shuttingDown = true;
            cleanup();
        }));
    }

    private void run() {
        while (!shuttingDown) {
            // Application loop
            processNextTask();
        }
    }

    private void cleanup() {
        System.out.println("Cleaning up resources");
        // Close files, connections, etc.
    }

    private void processNextTask() {
        // Task processing
    }
}
// Distributed tracing context propagation during shutdown
public class TracingShutdown {
    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            // Export tracing data before exit
            Tracing.flush();
        }));
    }
}
// Health check endpoint for graceful load balancer removal
public class HealthCheckServer {
    private volatile boolean healthy = true;

    public void start() {
        // Register shutdown hook to set unhealthy
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            healthy = false;
            // Give load balancer time to remove from pool
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }));

        // Start HTTP server on /health
    }

    public boolean isHealthy() {
        return healthy;
    }
}

Observability Checklist

  • Monitor JVM uptime to detect unexpected restarts
  • Log shutdown hook execution times to detect slow shutdowns
  • Track graceful vs. forced shutdown ratio
  • Monitor for SIGKILL (immediate termination) occurrences
  • Ensure heap dumps on OOM are written before shutdown
  • Verify shutdown hooks complete before process exit
  • Track finalizer queue depth if using deprecated finalize()
  • Monitor for native memory leaks that prevent clean shutdown
  • Loguncaught exceptions in shutdown hooks

Security Notes

Shutdown hooks run with full JVM privileges. Malicious shutdown hooks can execute arbitrary code. Ensure shutdown hooks are registered only by trusted code.

External processes that send signals to the JVM should be authorized. Anyone who can send SIGTERM to the JVM process can trigger shutdown hooks.

Do not store sensitive data in shutdown hook threads without synchronization. By the time shutdown hooks run, other application threads have terminated.

Common Pitfalls / Anti-Patterns

Registering shutdown hooks from library code. Libraries should never register shutdown hooks unless absolutely necessary. Library shutdown hooks can conflict with application shutdown hooks and cause unpredictable behavior.

Assuming shutdown is always graceful. SIGKILL (kill -9) terminates the JVM immediately without running shutdown hooks. Service managers like systemd send SIGTERM first, but processes can be killed with SIGKILL if they do not terminate quickly enough.

Using shutdown hooks for critical cleanup. Shutdown hooks are not guaranteed to run in all cases (JVM crash, SIGKILL, Runtime.halt()). Critical cleanup should happen synchronously before critical operations, not asynchronously at shutdown.

Blocking in shutdown hooks. If a shutdown hook blocks, the JVM waits indefinitely. Avoid any blocking operations: network calls, file I/O beyond what’s absolutely necessary, or acquiring locks that other threads might hold.

Forgetting that daemon threads do not trigger shutdown hooks to wait. If your application relies on non-daemon threads to prevent premature shutdown, remember that shutdown hooks run after those threads terminate. If those threads are holding resources needed by hooks, you have a problem.

Quick Recap Checklist

  • JVM bootstrap loads core classes, initializes GC and JIT, then runs main()
  • Shutdown hooks run when the JVM receives termination signal or System.exit()
  • All non-daemon threads must terminate before shutdown hooks run
  • Shutdown hooks run concurrently and independently
  • Do not call System.exit() from within a shutdown hook
  • Keep shutdown hooks fast and handle all exceptions
  • Runtime.halt() terminates immediately without running hooks
  • SIGKILL (-9) cannot be intercepted and bypasses shutdown hooks
  • Finalizers run after shutdown hooks complete
  • Daemon threads may terminate during shutdown without cleanup
  • Shutdown hooks are not guaranteed to run in all termination scenarios

Interview Questions

1. What is the order of events during JVM shutdown?

When shutdown is requested (via System.exit(), signal, or all non-daemon threads terminating), the JVM first determines if shutdown hooks are registered. If so, it spawns threads to run all registered shutdown hooks concurrently and waits for them to complete. After all shutdown hooks finish, the JVM runs any uninvoked finalizers (deprecated in Java 9+). Finally, the JVM calls _exit() to terminate the process. The key guarantee is that shutdown hooks are run even if the JVM terminates due to an error in the application code, but they are NOT run if the process is killed with SIGkill.

2. What happens if a shutdown hook calls System.exit()?

If a shutdown hook calls System.exit(), it causes a deadlock. The JVM has already started shutdown hook execution and is waiting for all hooks to complete before proceeding with the rest of the shutdown sequence. When System.exit() is called from a hook, the JVM attempts to start a new shutdown sequence, which cannot proceed because the current shutdown is still in progress. The JVM will hang indefinitely. The solution is to never call System.exit() from a shutdown hook. If you need to terminate from a hook, use Runtime.getRuntime().halt() instead.

3. How do you implement graceful shutdown of an ExecutorService?

Inside a shutdown hook, call executor.shutdown() to stop accepting new tasks. Then call executor.awaitTermination() with a timeout to wait for running tasks to complete. If the timeout expires, call executor.shutdownNow() to cancel running tasks and remove queued tasks from the queue. Always handle InterruptedException: if interrupted during awaitTermination, decide whether to continue waiting or cancel immediately. The shutdown hook should return (not block indefinitely) to allow the JVM to continue the shutdown sequence.

4. What signals does the JVM handle and how?

The JVM handles several signals: SIGTERM requests graceful shutdown, running shutdown hooks before terminating. SIGINT (Ctrl+C) also requests graceful shutdown. SIGQUIT requests a thread dump without terminating, useful for diagnostics. SIGABRT indicates the JVM crashed and triggers hs_err crash log generation. SIGKILL cannot be intercepted and terminates immediately. Signal handling is platform-dependent; Windows does not have signal equivalents for everything.

5. Why should you avoid using Object.finalize() and what should you use instead?

Object.finalize() is deprecated since Java 9 for several reasons: there is no guarantee when or if it runs, exceptions in finalizers can leave objects in a corrupt state, finalizers can resurrect objects (making them reachable again), and they impose performance overhead on every object allocation. Instead, use java.lang.ref.Cleaner for resources that must be cleaned up but should not block GC. For explicit resource cleanup (files, connections), use try-with-resources which guarantees cleanup at a specific point in code.

6. What happens during the JVM bootstrap phase before the main method executes?

The bootstrap phase involves: First, the OS loads the JVM shared library and creates the JVM process. Second, the bootstrap classloader loads core JDK classes from rt.jar (Java 8) or the modules image (Java 9+), including java.lang.Object, java.lang.Class, and all fundamental classes. Third, the JVM initializes internal subsystems: the garbage collector, interpreter, JIT compiler infrastructure, main thread and thread group, and classloader subsystem. Fourth, command line arguments are parsed. Fifth, shutdown hooks are registered. Sixth, the application main class is loaded and its static initializers run. Finally, the main method executes.

7. What is the difference between Runtime.halt() and System.exit() in shutdown hooks?

System.exit() initiates a complete JVM shutdown sequence, which includes running all registered shutdown hooks. If called from a shutdown hook, it attempts to start a nested shutdown sequence, causing a deadlock because the JVM is already waiting for all hooks to complete. Runtime.halt() terminates the JVM immediately without running any shutdown hooks. The only way to halt without running hooks is to call halt from outside the shutdown sequence entirely. Use halt only when you need to force termination and are certain no cleanup is needed or possible.

8. Why might a shutdown hook run multiple times or not at all?

Shutdown hooks run at most once per JVM instance because the JVM marks hooks as complete after execution. However, if the same hook thread is registered multiple times (for example, the same Runnable instance passed to addShutdownHook twice), the JVM treats them as separate hook entries and runs the hook multiple times. A shutdown hook may not run if: the JVM crashes (SIGABRT does not guarantee hook completion), the process is killed with SIGKILL, Runtime.halt() is called, or the operating system terminates the process forcefully.

9. How does the module system affect class loading order during JVM startup?

In Java 9+, the module system defines a strict ordering for class loading during startup. The boot module (java.base) is loaded first by the bootstrap classloader. Other JDK modules (java.sql, java.xml, etc.) are loaded as needed based on the module graph. Application modules are loaded after the JDK modules, following the requires-dependencies declared in each module's module-info.java. This means a module that requires another cannot be loaded before its dependency. Circular dependencies between modules are disallowed at runtime.

10. What is the -XX:+HandlePromotionFailure flag and why was it significant for GC?

The -XX:+HandlePromotionFailure flag controlled how the young generation collector handled promotion failures during garbage collection. A promotion failure occurred when the JVM tried to promote an object from young to old generation but found insufficient contiguous space. When enabled, the JVM would perform a "defensive" collection, copying fewer objects to old generation and keeping more in survivor spaces. This flag was significant for G1 and CMS collectors. Starting from Java 7u4, the HotSpot JVM made this behavior the default (always enabled), and the flag was eventually removed in newer Java versions.

11. What is the difference between initialization hooks and shutdown hooks?

Initialization hooks are threads that start during JVM initialization, before the application main method runs. They are useful for pre-warming caches, initializing native libraries, verifying external dependencies, or setting up JVMTI agents. Shutdown hooks are threads registered via Runtime.addShutdownHook() that run during JVM shutdown, after application threads have terminated but before process exit. They serve different lifecycle phases: startup vs. shutdown of the JVM process.

12. How does Flight Recorder integrate with JVM shutdown?

Java Flight Recorder (JFR) runs as a JVM-internal shutdown hook. When the JVM receives SIGABRT (crash) or normal shutdown, FSR dumps its recorded data to a file (`.jfr` file) before the process exits. This allows post-mortem analysis of what happened leading up to a crash. FSR data is written in a binary format that can be analyzed with JDK Mission Control. If the JVM crashes, the hs_err log and FSR dump provide complementary diagnostic information.

13. What are the five phases of JVM shutdown in order?

Phase 1: Shutdown requested (via System.exit(), signal, or all non-daemon threads ending). Phase 2: Running shutdown hooks—all registered hooks run concurrently; the JVM waits indefinitely for them to complete unless Runtime.halt() is called. Phase 3: Finalization—runs any uninvoked finalizers (deprecated in Java 9+). Phase 4: JVM termination—the JVM calls _exit() to terminate the process. Phase 5: OS-level process cleanup (kernel reclaims resources).

14. What happens when a static initializer causes an exception during class loading?

If a static initializer throws an exception that is not caught, the class is marked as having failed initialization. Subsequent attempts to use the class throw ExceptionInInitializerError wrapping the original exception. Other threads that were waiting for the class to initialize will also receive ExceptionInInitializerError. This prevents classes from being used in a partially-initialized state, which could cause hard-to-debug behavior. The original exception is preserved as the cause of ExceptionInInitializerError.

15. Why should shutdown hooks be fast and non-blocking?

The JVM waits for all shutdown hooks to complete before exiting. If any hook blocks indefinitely (waiting on network I/O, file I/O, or locks held by other threads), the JVM hangs on shutdown. Hooks that take too long delay process termination, which matters for container orchestrators and service managers expecting quick restarts. Best practice: perform cleanup synchronously during normal operation, not asynchronously at shutdown where hooks may never run (SIGKILL, crash).

16. What is the role of the VMThread during JVM shutdown?

The VMThread is the internal JVM thread responsible for executing VM operations, including shutdown sequence execution. When shutdown is triggered, the VMThread coordinates the shutdown hooks, finalization, and other VM-level cleanup tasks. It ensures these run in the proper order on dedicated hook threads. The VMThread is not directly accessible from Java code—it is internal to the JVM implementation.

17. How does JVM initialization interact with the Safety SE6?

During initialization, the JVM must ensure the core classes are properly loaded and linked before any user code runs. The bootstrap classloader loads java.lang.Object, java.lang.Class, and all fundamental types. Static fields get default values, and static initializers run in a specific order. The JVM verifies that core classes pass bytecode verification before executing any of their methods. This creates a trusted foundation on which all other Java code depends.

18. What happens if you register the same shutdown hook twice?

If the same Thread or Runnable instance is registered twice via addShutdownHook(), the JVM treats them as two separate hook entries. Both will run during shutdown, potentially causing duplicate cleanup operations. Design hooks to be idempotent if possible, or guard against double execution with a flag. The JVM does not deduplicate hook registrations.

19. How does -XX:+TraceClassInitialization help diagnose startup issues?

The -XX:+TraceClassInitialization flag prints a log line for each class as it is initialized, including the timestamp and any exception that occurred. This helps diagnose circular class initialization deadlocks or failures during startup. When classes fail to initialize, the trace shows which class's static initializer threw, allowing you to pinpoint the problem. Combine with GC logs for a complete picture of JVM startup behavior.

20. What is the relationship between daemon threads and shutdown hooks?

Daemon threads do not prevent the JVM from shutting down. When all non-daemon threads have terminated, the JVM proceeds to the shutdown hook phase regardless of daemon threads still running. However, daemon threads may be terminated at any point during shutdown hook execution, which can cause problems if hooks depend on daemon threads for cleanup. Do not rely on daemon threads being alive during shutdown hooks.

Further Reading

Conclusion

JVM startup involves loading core classes, initializing the runtime system (GC, JIT, threads), and executing your main method. Shutdown runs registered hooks concurrently before finalization and process exit. Implement graceful shutdown by registering hooks that clean up resources, never call System.exit() from a hook, and keep hooks fast and non-blocking. SIGKILL bypasses all shutdown handling.

Category

Related Posts

GraalVM Native Image

Understand ahead-of-time compilation with GraalVM Native Image and the Substrate VM runtime for near-instant startup and minimal memory footprint.

#graalvm #native-image #aot

Async-Profiler: Low-Overhead CPU and Memory Profiling

Learn async-profiler for low-overhead CPU and memory profiling in production. Generate flame graphs, analyze allocations, and diagnose JVM bottlenecks.

#jvm #async-profiler #cpu-profiling

Java Atomics and VarHandle: Low-Level Concurrency

Understanding Java atomic operations: AtomicInteger, AtomicReference, VarHandle, compareAndSet, atomics vs locks, and lock-free programming patterns.

#java #jvm #concurrency