什么情况会导致 JVM 退出?


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

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

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. JVM 生命周期理解:面试官想看你是否清楚 JVM 从启动到退出的完整生命周期,以及哪些情况属于 "正常退出",哪些属于 "异常终止"。

  2. Shutdown Hook 认知:是否了解 JVM 退出前的钩子机制,这在生产环境中做资源清理(关闭连接池、刷缓冲区)非常关键。

  3. 线上排查意识:如果你经历过 OOM 崩溃、kill -9 导致服务无响应这些场景,说明你是有实战经验的,面试官会加分。

核心答案

导致 JVM 退出的情况可以分为 三大类

类别 具体场景 是否执行 Shutdown Hook 退出码
正常退出 System.exit()、最后一个非守护线程结束、Runtime.halt() exit() 执行,halt() 不执行 0 或指定值
外部强制 kill(SIGTERM)、Ctrl+C(SIGINT)、kill -9(SIGKILL) killCtrl+C 执行,kill -9 不执行 128+信号编号
异常崩溃 OOM、StackOverflow、JVM 内部错误、JNI 崩溃 不一定,取决于崩溃类型 非 0

深度解析

一、正常退出

1. System.exit(int status)

最直接的退出方式。调用后 JVM 会触发 Shutdown Hook(后面细说),然后退出。status = 0 表示正常退出,非 0 表示异常退出。

public static void main(String[] args) {
    // 注册 Shutdown Hook
    Runtime.getRuntime().addShutdownHook(new Thread(() -> {
        System.out.println("JVM 要退出了,做点清理工作...");
    }));

    System.out.println("程序执行完毕");
    System.exit(0); // 触发 Shutdown Hook 后退出
}

有个冷知识:System.exit() 底层调的是 Runtime.getRuntime().exit(),而且如果安全管理器(SecurityManager)开启了,没权限的话会抛 AccessControlException。不过 JDK 17 之后 SecurityManager 已经标记为废弃了。

2. 最后一个非守护线程结束

当所有非守护线程(也叫用户线程)都执行完毕后,JVM 会自动退出。守护线程(Daemon Thread)不会阻止 JVM 退出。

public static void main(String[] args) {
    Thread t = new Thread(() -> {
        // 守护线程
        while (true) {
            try { Thread.sleep(1000); } catch (InterruptedException e) {}
            System.out.println("守护线程还在跑...");
        }
    });
    t.setDaemon(true); // 设为守护线程
    t.start();

    // main 线程是非守护线程
    // main 结束后,即使守护线程还在跑,JVM 也会退出
}

这个机制很多人容易忽略。上面代码中,main 线程一结束,JVM 就退出了,守护线程直接被 "腰斩"。所以如果你把数据库连接池的维护线程设成了守护线程,那就等着线上事故吧。

3. Runtime.getRuntime().halt(int status)

这个方法比较 "暴力"——它 强制 退出 JVM,不会执行 Shutdown Hook,也不会执行 finalizer。一般只在 Shutdown Hook 里防止死锁导致 JVM 无法退出时使用。

二、外部强制终止

1. kill PID(发送 SIGTERM 信号)

这是最常用的优雅停机方式。JVM 收到 SIGTERM 后,会执行 Shutdown Hook,然后退出。Spring Boot 的优雅停机就依赖这个机制。

2. Ctrl+C(发送 SIGINT 信号)

开发调试时常用。效果和 kill 类似,JVM 会执行 Shutdown Hook 后退出。

3. kill -9 PID(发送 SIGKILL 信号)

这是 "核弹级" 操作。JVM 立刻被杀死,不会执行 Shutdown Hook,不会执行 finally 块,正在写的文件可能损坏,连接池不会释放。生产环境不到万不得已别用。

上表汇总了各种终止方式的对比。有几个要点:

  • finally 块只在 当前线程 正常或异常退出时执行,JVM 整体退出时是不会等 finally
  • Shutdown Hook 是 JVM 退出前的 "最后一道防线",但 kill -9halt() 会绕过它
  • OOM 崩溃时,JVM 会尝试执行 Shutdown Hook,但此时内存已经不够了,Hook 可能执行失败

三、异常崩溃

1. OutOfMemoryError

堆内存耗尽时抛出。JVM 可能会尝试执行 Shutdown Hook,但因为内存不足,Hook 可能跑不起来。常见的 OOM 场景:

  • 堆内存溢出:Java heap space
  • 元空间溢出:Metaspace
  • 直接内存溢出:Direct buffer memory
  • 无法创建新线程:unable to create new native thread

2. StackOverflowError

线程栈溢出,通常是递归调用太深。单个线程的 StackOverflowError 不一定会导致 JVM 退出,但如果它发生在 main 线程且没有被捕获,JVM 就会退出。

3. JVM 内部错误

Internal ErrorUnknown Error,这种一般是 JVM 自身的 bug,比如 JIT 编译器错误、GC 出问题等。遇到这种情况基本只能升级 JDK 版本。

4. JNI 崩溃

调用了本地方法(C/C++ 代码),如果本地代码有段错误(Segmentation Fault),JVM 直接崩溃,生成 hs_err_pid.log 文件。

四、Shutdown Hook 机制

既然提到了好几次 Shutdown Hook,展开讲一下。它是 JVM 提供的 "优雅退出" 机制:

// 注册多个 Shutdown Hook
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    System.out.println("Hook 1: 关闭数据库连接池...");
}, "shutdown-hook-1"));

Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    System.out.println("Hook 2: 刷新缓存到磁盘...");
}, "shutdown-hook-2"));

Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    System.out.println("Hook 3: 关闭网络连接...");
}, "shutdown-hook-3"));

几个关键点:

  • 多个 Hook 会 并发执行,顺序不保证。所以 Hook 之间不能有依赖关系
  • Hook 执行过程中如果调用 System.exit(),会导致死锁——JVM 会直接 halt
  • Hook 应该 ,别在里面做耗时操作,否则可能被外部强制杀掉

面试高频追问

  1. 追问:如何实现 Java 应用的优雅停机?

    核心思路:注册 Shutdown Hook 做资源清理 + 使用 kill(而非 kill -9)发送信号 + Spring Boot 配置 server.shutdown=graceful。如果是容器环境(K8s),还需要配置 preStop 钩子给 JVM 留足够的清理时间。

  2. 追问:守护线程和用户线程的区别?

    核心区别就一个:守护线程不影响 JVM 退出。当所有用户线程都结束了,JVM 就退出,不管守护线程还在干什么。典型的守护线程就是 GC 线程。

  3. 追问:线上服务突然挂了怎么排查?

    看几个地方:JVM 的 hs_err_pid.log(如果存在说明是崩溃)、应用的 GC 日志(是否有 OOM)、/var/log/messagesdmesg(是否被 OOM Killer 杀掉)、容器日志(是否被 K8s 杀掉)。

常见面试变体

  • "JVM 的 Shutdown Hook 是什么?"
  • "Java 程序如何实现优雅停机?"
  • "守护线程和用户线程有什么区别?"
  • "System.exit()Runtime.halt() 的区别?"

记忆口诀

JVM 退出三兄弟:正常走exit()、线程结束)、被杀掉killkill -9)、自己崩(OOM、StackOverflow、JNI 错误)。记住 kill -9halt() 是 "核弹",Shutdown Hook 也救不了。

总结

JVM 退出的原因归结起来就三类:正常退出、外部强制终止、异常崩溃。面试中重点答清楚 System.exit() 和非守护线程结束这两种正常退出,再提到 OOM 崩溃和 kill 信号的影响,最后带上 Shutdown Hook 机制,基本就是满分回答。生产环境中最关键的是理解 killkill -9 的区别,确保优雅停机。