Skip to content

JVM 的生命周期

JVM 也有生老病死

和普通程序一样,JVM 也有自己的生命周期。它不是从你写代码的那一刻就存在的,而是从你运行 java 命令时诞生,随着程序结束或异常退出而消亡。

理解 JVM 的生命周期,有助于你理解程序启动和退出的过程,也能帮助你在排查问题时判断「是应用崩了还是 JVM 崩了」。

JVM 的启动

运行 java -jar app.jar,操作系统会启动一个进程,这个进程里跑的就是 JVM。

启动流程

用户执行 java 命令


JVM 创建(装载 native 的 launcher 代码)


创建堆内存、设置 GC 策略


加载引导类(Bootstrap ClassLoader)


执行 main() 方法 → Java 程序开始运行

JVM 启动时,会经历以下步骤:

  1. 创建 JVM 实例:操作系统分配进程内存,JVM 加载 native 代码(负责初始化)
  2. 创建堆:根据 -Xmx-Xms 参数设置堆大小
  3. 选择 GC 策略:根据机器配置和 JVM 参数选择合适的垃圾回收器
  4. 加载引导类:Bootstrap ClassLoader 加载 java.lang.Stringjava.lang.Thread 等核心类
  5. 执行 main 方法:JVM 创建一个主线程,调用 main(String[] args)

注意:main() 方法的执行才是 Java 程序的开始,而不是 java 命令执行的那一刻。java 命令到 main() 执行之间,JVM 做了大量的初始化工作。

java
public class LifeCycleDemo {
    public static void main(String[] args) {
        // 从这里开始,是 Java 程序真正运行的地方
        System.out.println("Hello from main thread");
    }
}

main 线程的创建

当 JVM 执行 main 方法时,首先会创建一个主线程:

java
// JVM 内部大致等价于:
Thread mainThread = new Thread(() -> {
    // 执行 main 方法的字节码
    LifeCycleDemo.main(args);
});
mainThread.start();

这个主线程和其他所有线程一样,有自己的 PC 寄存器、虚拟机栈。如果主线程结束了,java 进程不一定退出——如果有其他非守护线程还在运行,JVM 会继续存活。

运行阶段

Java 程序运行期间,JVM 负责:

  • 加载类:按需加载 .class 文件(懒加载)
  • 执行字节码:解释执行 + JIT 编译执行
  • 内存管理:对象分配、垃圾回收
  • 线程调度:多线程并发执行
  • 异常处理:捕获和传播异常

这个阶段是 JVM 最「忙」的时候。

线程与 JVM 的关系

Java 程序可以创建任意多个线程,每个线程都会占用一定的资源(PC 寄存器、虚拟机栈)。

java
public class MultiThreadDemo {
    public static void main(String[] args) {
        // 创建一个新的工作线程
        Thread worker = new Thread(() -> {
            // 这个线程有自己独立的 PC 寄存器和虚拟机栈
            System.out.println("Worker thread is running");
        });
        worker.start();

        // 主线程继续执行
        System.out.println("Main thread is running");
    }
}

所有用户线程都是 JVM 的子线程——JVM 自己也有内部的 GC 线程、编译器线程。它们共享堆和方法区,但各有独立的栈。

JVM 的退出

JVM 的退出条件有两个,满足其一即可:

1. 所有非守护线程结束

这是正常退出的方式。当 main 方法执行完毕,并且程序中创建的所有非守护线程都结束时,JVM 会正常退出。

java
public class ExitDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("main start");

        Thread t = new Thread(() -> {
            System.out.println("worker start");
            try { Thread.sleep(1000); } catch (InterruptedException e) {}
            System.out.println("worker end");
        });
        t.start();

        System.out.println("main end");
        // main 线程结束后,JVM 不退出
        // 因为 t 是非守护线程,还在运行
        // 只有 t 结束后,JVM 才退出
    }
}

2. 调用 System.exit()

这是主动退出。可以调用 System.exit(int status) 强制终止 JVM:

java
public class ForceExit {
    public static void main(String[] args) {
        try {
            // 模拟某些初始化工作
            init();
            // 启动应用
            start();
        } finally {
            // 无论发生什么,最后清理并退出
            cleanup();
            System.exit(0);
        }
    }
}

3. 异常或错误导致 JVM 终止

这是非正常退出。比如:

  • OutOfMemoryError:堆内存耗尽
  • StackOverflowError:栈深度超限
  • JVM 内部错误(虚拟机错误,InternalError
java
public class AbnormalExit {
    public static void main(String[] args) {
        // 无限递归 → StackOverflowError → JVM 退出
        recursive();
    }

    static void recursive() {
        recursive();  // 没有终止条件
    }
}

4. 外部强制终止

操作系统层面用 kill -9 直接杀掉进程,或者 IDE 的「停止」按钮,都属于外部强制终止。这种情况下 JVM 的 shutdown hook 不会执行。

Shutdown Hook:JVM 退出前的最后机会

JVM 提供了一种机制,在真正退出之前执行一些清理工作——Shutdown Hook

java
public class ShutdownHookDemo {
    public static void main(String[] args) {
        // 注册一个关闭钩子
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("Shutdown hook executed!");
            // 释放资源、关闭文件、发送告警等
        }));

        System.out.println("Application is running");
        // 模拟业务逻辑
        try { Thread.sleep(2000); } catch (InterruptedException e) {}
        System.out.println("Application finished normally");
    }
}

Shutdown Hook 的执行时机:

  • 所有正常线程(User Daemon)都结束后
  • System.exit() 被调用时
  • 不会执行:外部 kill -9、JVM 崩溃、Runtime.halt()

常用于:释放资源、关闭连接、输出日志、发送告警等。

JVM 退出与守护线程

Java 线程分为守护线程(Daemon Thread)用户线程(User Thread)

  • 守护线程:为其他线程提供服务,如 GC 线程。JVM 不等待守护线程退出。
  • 用户线程:执行具体业务逻辑。JVM 必须等待所有用户线程退出才退出。
java
public class DaemonThreadDemo {
    public static void main(String[] args) {
        Thread daemon = new Thread(() -> {
            while (true) {
                // 模拟监控任务
                monitor();
            }
        });
        // 设置为守护线程
        daemon.setDaemon(true);
        daemon.start();

        // 主线程结束
        System.out.println("main thread finished");
        // JVM 不会等待 daemon 线程,会直接退出
    }
}

注意:如果主线程在创建 daemon 线程后立即退出,用户线程数为 0,JVM 会退出。daemon 线程还没来得及打印任何东西,进程就结束了。所以上面的代码可能什么都看不到。

JVM 进程与 Java 进程的关系

一个 Java 程序的进程结构:

操作系统进程

  ├── JVM 进程(native)
  │     ├── GC 线程(守护线程)
  │     ├── 编译器线程(守护线程)
  │     ├── 信号处理线程
  │     └── ...

  ├── 主线程(用户线程)
  ├── Worker-1(用户线程)
  ├── Worker-2(用户线程)
  └── ...

JVM 崩溃(native 层出错)会导致整个进程退出,而不是仅仅退出 Java 代码。java.lang.OutOfMemoryError 不会导致 JVM 退出(JVM 还能跑),但 SIGSEGV(段错误)会导致 JVM 崩溃。

本节小结

JVM 的一生:

启动(java 命令) → 初始化(堆、GC、引导类) → 运行(执行 main)

                                    所有非守护线程结束 或 System.exit()

                                         退出前(Shudown Hook)

                                               退出

关键点:

  • JVM 启动后才开始执行 main(),之前是初始化
  • JVM 退出 = 所有非守护线程结束,或 System.exit()
  • Shutdown Hook 是退出前的最后清理机会
  • 守护线程不阻塞 JVM 退出,但用户线程会

到这里,「JVM 基础认知」部分的核心概念就讲完了。接下来,我们要进入 JVM 最核心、最复杂的部分——主流 JVM 虚拟机介绍,了解各路虚拟机的来龙去脉。

基于 VitePress 构建