什么情况会导致 JVM 退出?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

面试官问这个问题,通常希望考察你对 JVM 生命周期的整体理解,而不仅仅是背出几个点。具体来说,他想知道:

  1. 你对线程类型(守护线程 vs 非守护线程)的理解
    • 是否清楚 JVM 在最后一个非守护线程结束后会自动退出。
  2. 你对主动退出 API 的掌握程度
    • 是否知道 System.exit()Runtime.exit()Runtime.halt() 的区别和各自的行为。
  3. 你对 JVM 关闭钩子(Shutdown Hook)的了解
    • 是否知道在什么情况下关闭钩子会被执行,什么情况下不会。
  4. 你对 JVM 异常退出场景的认识
    • 比如致命错误(OutOfMemoryError)、虚拟机内部错误、本地方法调用崩溃等。
  5. 你在实际开发中是否考虑过优雅停机
    • 是否知道如何通过信号处理、关闭钩子等方式实现资源清理和优雅下线。
  6. 你是否能区分正常退出与强制退出
    • 比如 SIGTERMSIGKILL 对 JVM 的不同影响。

核心答案

JVM 退出(即 Java 进程终止)主要发生在以下几种情况:

  • 所有非守护线程执行完毕(正常退出)
  • 主动调用 System.exit(int status)Runtime.exit(int status)
  • 主动调用 Runtime.halt(int status)(直接终止,不执行关闭钩子)
  • JVM 内部发生致命错误(如 OutOfMemoryError 无法恢复、Stack Overflow 导致崩溃、本地代码错误等)
  • 外部信号强制终止进程(如 kill -9、系统关机、容器 OOM Kill 等)

深度解析

原理 / 机制

JVM 的退出流程本质上与线程的生命周期紧密相关。

  • 非守护线程(User Thread):JVM 启动时会创建一个 main 线程(非守护线程)。只要还有任何一个非守护线程存活,JVM 就不会退出。当最后一个非守护线程结束时,JVM 便会启动退出过程。
  • 守护线程(Daemon Thread):守护线程是为非守护线程提供服务的,JVM 退出时不会等待守护线程执行完毕,它们会随 JVM 一起被强制终止。
  • 主动退出 API
    • System.exit(int status):内部调用 Runtime.getRuntime().exit(status),会触发关闭钩子(Shutdown Hook)的执行,然后调用 halt() 最终终止 JVM。
    • Runtime.halt(int status):直接终止 JVM,不会执行任何关闭钩子或终结器(finalizer)。
  • 致命错误:当 JVM 遇到无法恢复的内部错误(如内存分配失败、字节码验证错误、本地方法崩溃)时,可能会直接生成 crash 日志并退出。
  • 外部信号
    • SIGTERM(kill 默认发送)可以被 JVM 捕获,触发关闭钩子后退出。
    • SIGKILL(kill -9)无法捕获,进程被操作系统直接强制终止,关闭钩子不会执行。

代码示例

示例 1:非守护线程与守护线程对退出的影响

public class JVMExitDemo {
    public static void main(String[] args) {
        // 创建一个非守护线程
        Thread userThread = new Thread(() -> {
            try { Thread.sleep(2000); } catch (InterruptedException e) { }
            System.out.println("用户线程结束");
        });
        userThread.start();

        // 创建一个守护线程
        Thread daemonThread = new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(500);
                    System.out.println("守护线程运行中");
                } catch (InterruptedException e) { }
            }
        });
        daemonThread.setDaemon(true);
        daemonThread.start();

        System.out.println("main 线程结束");
        // main 线程结束后,JVM 会等待 userThread 结束,不会等待 daemonThread
    }
}

运行后会发现,main 线程和守护线程都会持续输出,但 userThread 结束后 JVM 立即退出,守护线程被强制终止。

示例 2:关闭钩子的使用

public class ShutdownHookDemo {
    public static void main(String[] args) {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("执行关闭钩子:清理资源...");
        }));

        System.out.println("应用运行中,按 Ctrl+C 或调用 System.exit() 会触发钩子");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) { }
        System.exit(0); // 会触发钩子
        // Runtime.getRuntime().halt(0); // 不会触发钩子
    }
}

对比分析

退出方式是否执行关闭钩子是否执行终结器典型触发场景
最后一个非守护线程结束✅ 是✅ 是正常业务结束
System.exit() / Runtime.exit()✅ 是✅ 是主动退出,比如命令行工具完成任务
Runtime.halt()❌ 否❌ 否紧急终止,绕过清理逻辑
致命错误(如 OOM)❌ 通常不执行❌ 否内存泄漏、栈溢出等
外部信号 SIGTERM(kill)✅ 是✅ 是容器停止、进程终止请求
外部信号 SIGKILL(kill -9)❌ 否❌ 否强制杀死进程

最佳实践

  1. 避免滥用 System.exit()
    • 在 Web 应用或框架代码中调用 System.exit() 会导致整个容器退出,应通过返回码或异常向上层传递终止信号。
  2. 利用关闭钩子实现优雅停机
    • 在应用启动时注册关闭钩子,用于释放数据库连接、关闭文件流、通知注册中心下线等。
    • 注意关闭钩子的执行时间有限(默认几秒),超时后会被强制终止。
  3. 合理设计线程生命周期
    • 主线程结束后,如果仍需执行后台任务,应使用非守护线程,避免因所有非守护线程结束而意外退出。
  4. 处理信号实现自定义退出逻辑
    • 可以通过 sun.misc.Signal 或第三方库捕获 SIGTERM,执行自定义操作后再调用 System.exit()
  5. 监控 JVM 崩溃日志
    • 当 JVM 因致命错误退出时,会生成 hs_err_pid.log 文件,应定期检查这些日志以定位问题。

常见误区

  • 误区一:认为所有线程结束 JVM 才退出
    实际上 JVM 只等待非守护线程,守护线程会被强制终止。
  • 误区二:混淆 exithalt
    halt 会跳过所有清理动作,可能导致资源泄漏或数据不一致,应谨慎使用。
  • 误区三:认为关闭钩子一定会执行
    halt()kill -9、JVM 崩溃等场景下,关闭钩子不会被执行。
  • 误区四:认为 OutOfMemoryError 一定会导致 JVM 退出
    如果 OOM 发生在某个线程中,该线程通常会被终止,但如果还有其他非守护线程存活,JVM 可能不会退出。不过多数生产环境会因内存不足而无法继续正常服务,最终触发致命错误退出。

总结

JVM 退出的本质是最后一个非守护线程结束,或者显式调用了终止方法。理解这一点,并结合关闭钩子、信号处理等机制,可以帮助我们编写更健壮的 Java 应用,在停机时做好资源清理,避免数据丢失或不一致。同时,要区分正常退出与强制退出,避免在代码中意外终止整个 JVM 进程。