Skip to content

main线程与JVM退出条件 ​

Question ​

Main方法启动子线程执行任务,Main执行结束,那么程序执行结束了么?

一句话答案 ​

没有。main 只是 JVM 启动时创建的入口线程,不是进程的"主人"。JVM 退出的条件是:所有非 daemon 线程都运行结束,与 main 结不结束无关。只要子线程不是 daemon 线程,main 先走一步,JVM 也会继续活着,等它跑完才退出。

一、JVM 的退出条件 ​

Java 语言规范对初始退出条件的描述可以概括为:当 JVM 中只剩 daemon 线程在运行时,JVM 正常终止。即:

  • main 线程没有任何特殊地位,它结束只是"少了一个非 daemon 线程";
  • 判断进程活不活,数的是非 daemon 线程的个数:还有 1 个,JVM 就不退;
  • daemon 线程不阻止退出——JVM 走的时候直接把它们抛弃(见第四节陷阱);
  • 另外两条提前退出路径与此无关:代码里调 System.exit(status),或进程被 kill/发生 OOM 等致命错误。

二、main 与子线程的生命周期对照 ​

main ──┬── new Thread(task) ── start() ── 打印"main 结束" ──╳ main 死亡
       │
child  └────────── 仍在运行 tick...tick... ──────────────╳ 子线程结束
                                                          ▲
                                          JVM 等到它才退出进程
维度main 线程子线程
创建者JVM(依据 main 入口)应用代码
角色入口/启动线程,跑完就退与 main 平级的普通线程
栈独立线程栈独立线程栈
结束的影响不影响子线程继续运行非 daemon 未结束则 JVM 不退出

线程之间是平级关系,"父子"只体现在创建瞬间传承的少量属性:daemon 标志、优先级、contextClassLoader、InheritableThreadLocal。运行期互相不依赖,谁先死都不影响对方。

三、代码验证 ​

java
public class MainAndChild {
    public static void main(String[] args) {
        Thread child = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("child tick " + i);
                try { Thread.sleep(1000); } catch (InterruptedException e) { return; }
            }
            System.out.println("child done");
        }, "child-1");
        child.start();
        System.out.println("main 线程执行完毕:" + Thread.currentThread().getName());
    }
}

输出顺序:

main 线程执行完毕:main
child tick 1
child tick 2
child tick 3
child tick 4
child tick 5
child done          ← 进程直到这里才退出

main 打印完就结束了,但进程多活了 5 秒。期间用 jps 能看到进程还在,jstack 里 main 线程已消失、child-1 还在 TIMED_WAITING (sleeping)。想让 main 等子线程再退,调 child.join() 即可改变时序。

四、daemon 线程与它的陷阱 ​

把子线程设为 daemon,JVM 就不必等它:

java
Thread t = new Thread(runnable);
t.setDaemon(true);   // 必须在 start() 之前调用,否则抛 IllegalThreadStateException
t.start();

陷阱一:finally 不保证执行。JVM 退出时直接终止 daemon 线程,不走完 catch/finally:

java
Thread t = new Thread(() -> {
    try {
        Thread.sleep(60_000);
    } finally {
        flushToFile();   // main 先结束触发 JVM 退出时,这里不会被调用,数据丢失
    }
});
t.setDaemon(true);
t.start();

陷阱二:别让守护线程干落盘的活。日志异步刷盘、缓冲落库这类任务放 daemon 里,进程退出就可能丢最后一批数据。Logback 异步 Appender 专门用 shutdownHook 处理这件事。

陷阱三:线程池默认非 daemon。Executors.newFixedThreadPool() 的工作线程都是普通线程,单测里建了池忘 shutdown,测试方法结束后 JVM 迟迟不退,就是这个原因。

适用判断:心跳、监控上报这类"丢了无所谓"的后台任务用 daemon。凡是结果要落地的任务,不用。

五、实战清单 ​

  • main 结束 ≠ 程序结束:想等子线程做完再退,用 join() 或 CountDownLatch;
  • 想让后台任务不阻止退出:setDaemon(true) 且必须在 start 前调用;
  • 线程池用完要 shutdown(),测试代码可用自定义 daemon ThreadFactory 方便快速退出;
  • 停线程的正确姿势是 interrupt 协作式停止,见同目录 如何正确地停止一个线程?。

延伸阅读:如何正确地停止一个线程?、关于并发工具包——线程安全的 List、Map、Set。

2026-10 扩充(原内容过薄)。