什么情况会导致 JVM 退出?
结论先行:JVM 退出的本质是「所有非守护线程结束」或「有人主动终止进程」。常见路径有四类:正常结束(main 返回且无非守护线程)、主动调用(System.exit、Runtime.halt)、异常终止(未捕获异常清空唯一非守护线程、OOM 后 ExitOnOutOfMemoryError)、外部信号(SIGTERM、SIGKILL、容器 OOMKilled)。区分它们的关键是退出码和是否执行关闭钩子(shutdown hook)。
一、退出路径对照
| 路径 | 是否执行 shutdown hook | 典型退出码 | 说明 |
|---|---|---|---|
| main 返回且无非守护线程 | 是 | 0 | 最正常的退出 |
System.exit(n) | 是 | n | 触发钩子后退出,finally 不会执行 |
Runtime.halt(n) | 否 | n | 强制退出,钩子也不跑 |
| 未捕获异常 | 是 | 1 | 只有「最后一条非守护线程」被异常终结时才退出 |
-XX:+ExitOnOutOfMemoryError | 是 | 1 | OOM 直接退出,交给编排系统重启 |
kill -15(SIGTERM) | 是 | 143 | 容器停止、docker stop 的默认信号 |
kill -9(SIGKILL) | 否 | 137 | 无法被捕获,进程直接消失 |
| 容器 OOMKilled | 否 | 137 | cgroup 限内存被杀,JVM 来不及做任何事 |
| JVM 崩溃(SIGSEGV) | 否 | 134 / 139 | 生成 hs_err_pid.log |
线程视角:
非守护线程集合为空?
├─ 是 → 触发 Shutdown.exit → 执行所有 shutdown hook → 终止
└─ 否 → 继续运行(哪怕 main 早已返回)
二、非守护线程才是「活着」的标准
public class ExitByThread {
public static void main(String[] args) {
Thread t = new Thread(() -> {
try {
Thread.sleep(3000);
System.out.println("工作线程结束");
} catch (InterruptedException ignored) {
}
});
t.setDaemon(false); // 改成 true,JVM 会立刻退出,打印不出来
t.start();
System.out.println("main 返回");
}
}
// 输出:
// main 返回
// (约 3 秒后)工作线程结束
线上线程池的线程默认是非守护线程,所以只要池没关,JVM 就不会自己退出——这正是「服务停不下来」的常见原因。
三、关闭钩子与优雅停机
public class GracefulShutdown {
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("开始优雅停机:停止接流量、提交偏移、关闭池");
try {
Thread.sleep(500); // 模拟等待请求处理完
} catch (InterruptedException ignored) {
}
System.out.println("钩子执行完毕");
}, "shutdown-hook"));
System.out.println("启动完成,等待 SIGTERM");
}
}
java GracefulShutdown & # 后台启动
kill -15 $! # SIGTERM 会触发钩子
# 输出:启动完成,等待 SIGTERM
# 开始优雅停机:停止接流量、提交偏移、关闭池
# 钩子执行完毕
钩子里不要做重活:JVM 给钩子的时间有限(容器默认 30 秒 terminationGracePeriodSeconds),超时会被 SIGKILL,钩子被打断后数据可能不一致。
四、常见追问
| 追问 | 回答要点 |
|---|---|
System.exit 会执行 finally 吗 | 不会,System.exit 内部调用 Shutdown.exit 直接终止流程,finally 被跳过 |
| OOM 后进程一定会退出吗 | 不一定,默认只是抛异常;只有 ExitOnOutOfMemoryError 或异常冒泡到主线程才退出 |
| 137 和 143 怎么区分 | 137 = 128+9,被 SIGKILL 或 cgroup OOMKilled;143 = 128+15,收到 SIGTERM 正常停机 |
| 容器里为什么总是 137 | 堆外内存(直接内存、元空间、线程栈)没算进 -Xmx,超出 cgroup limit 就被杀 |
| 怎么排查意外退出 | 先看容器事件与 dmesg(OOMKilled),再看 hs_err_pid*.log,最后看启动参数是否配了 ExitOnOutOfMemoryError |
小结
JVM 退出看两件事:非守护线程是否清空、退出码是多少。0/1 是主动路径,137/143 是外部信号,134/139 是崩溃,配合 shutdown hook 与 hs_err 日志就能还原现场。