什么情况会导致 JVM 退出?
2026年02月25日
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 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 生命周期的整体理解,而不仅仅是背出几个点。具体来说,他想知道:
- 你对线程类型(守护线程 vs 非守护线程)的理解
- 是否清楚 JVM 在最后一个非守护线程结束后会自动退出。
- 你对主动退出 API 的掌握程度
- 是否知道
System.exit()、Runtime.exit()、Runtime.halt()的区别和各自的行为。
- 是否知道
- 你对 JVM 关闭钩子(Shutdown Hook)的了解
- 是否知道在什么情况下关闭钩子会被执行,什么情况下不会。
- 你对 JVM 异常退出场景的认识
- 比如致命错误(
OutOfMemoryError)、虚拟机内部错误、本地方法调用崩溃等。
- 比如致命错误(
- 你在实际开发中是否考虑过优雅停机
- 是否知道如何通过信号处理、关闭钩子等方式实现资源清理和优雅下线。
- 你是否能区分正常退出与强制退出
- 比如
SIGTERM和SIGKILL对 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) | ❌ 否 | ❌ 否 | 强制杀死进程 |
最佳实践
- 避免滥用
System.exit()- 在 Web 应用或框架代码中调用
System.exit()会导致整个容器退出,应通过返回码或异常向上层传递终止信号。
- 在 Web 应用或框架代码中调用
- 利用关闭钩子实现优雅停机
- 在应用启动时注册关闭钩子,用于释放数据库连接、关闭文件流、通知注册中心下线等。
- 注意关闭钩子的执行时间有限(默认几秒),超时后会被强制终止。
- 合理设计线程生命周期
- 主线程结束后,如果仍需执行后台任务,应使用非守护线程,避免因所有非守护线程结束而意外退出。
- 处理信号实现自定义退出逻辑
- 可以通过
sun.misc.Signal或第三方库捕获SIGTERM,执行自定义操作后再调用System.exit()。
- 可以通过
- 监控 JVM 崩溃日志
- 当 JVM 因致命错误退出时,会生成
hs_err_pid.log文件,应定期检查这些日志以定位问题。
- 当 JVM 因致命错误退出时,会生成
常见误区
- ❌ 误区一:认为所有线程结束 JVM 才退出
实际上 JVM 只等待非守护线程,守护线程会被强制终止。 - ❌ 误区二:混淆
exit和halt
halt会跳过所有清理动作,可能导致资源泄漏或数据不一致,应谨慎使用。 - ❌ 误区三:认为关闭钩子一定会执行
在halt()、kill -9、JVM 崩溃等场景下,关闭钩子不会被执行。 - ❌ 误区四:认为
OutOfMemoryError一定会导致 JVM 退出
如果 OOM 发生在某个线程中,该线程通常会被终止,但如果还有其他非守护线程存活,JVM 可能不会退出。不过多数生产环境会因内存不足而无法继续正常服务,最终触发致命错误退出。
总结
JVM 退出的本质是最后一个非守护线程结束,或者显式调用了终止方法。理解这一点,并结合关闭钩子、信号处理等机制,可以帮助我们编写更健壮的 Java 应用,在停机时做好资源清理,避免数据丢失或不一致。同时,要区分正常退出与强制退出,避免在代码中意外终止整个 JVM 进程。