JVM不退出是因为存在未终止的非守护线程;需用jstack检查RUNNABLE/WAITING且daemon=false的线程,显式关闭Timer、线程池、连接池等资源,并排查阻塞的ShutdownHook。

OpenJDK 程序执行完 main 方法后不退出,基本可以断定:JVM 里还有非守护线程(non-daemon thread)在运行,或者 ShutdownHook 被阻塞,又或者存在未关闭的资源(如 Netty EventLoop、数据库连接池、RMI Registry)。这不是“程序卡住”,而是 JVM 主动维持运行状态。
怎么确认是不是还有活着的非守护线程
最直接的办法是用 jstack 抓当前线程快照:
jstack <pid></pid>
重点关注输出末尾的线程状态统计,比如:
Found 1 non-daemon thread(s)
然后往上翻,找到所有 java.lang.Thread.State: RUNNABLE 或 WAITING 但 daemon=false 的线程。常见嫌疑包括:
-
ForkJoinPool.commonPool-worker-*—— 没调用ForkJoinPool#shutdown()或没 awaitTermination -
DestroyJavaVM一直没出现 —— 说明主线程已结束,但其他线程还在 hold 住 JVM -
Reference Handler、Finalizer通常没问题;但若看到自定义线程名(如db-pool-thread-1、netty-eventloop-2),就要查对应资源是否释放
常见被忽略的资源泄漏点
很多“执行完不退出”其实和线程无关,而是某些库内部启了后台线程但没暴露关闭入口,或关闭逻辑没被触发:
-
java.util.Timer:必须显式调用timer.cancel(),否则它创建的TimerThread是非守护线程 -
ScheduledThreadPoolExecutor:即使任务全取消,也要调用shutdown()+awaitTermination() -
Log4j2:默认启用异步日志(AsyncLoggerContext),会启动后台线程;可通过System.setProperty("log4j2.shutdownCallbackRegistry", "none")或调用Configurator.shutdown()强制清理 -
HikariCP连接池:必须调用HikariDataSource#close(),否则其内部HouseKeeper线程持续运行
如何快速验证是不是 ShutdownHook 导致延迟退出
ShutdownHook 本身不会阻止 JVM 启动退出流程,但如果某个注册的钩子执行时间过长、抛出未捕获异常、或调用了阻塞操作(如网络请求、文件锁等待),就会拖慢整个退出过程,甚至让进程看起来“卡住”:
- 检查是否在
Runtime.getRuntime().addShutdownHook()里做了同步 I/O、远程调用或无超时的join() - 在钩子里加
Thread.currentThread().setName("my-shutdown-hook"),再用jstack看它是否处于WAITING或BLOCKED - 临时注释掉所有
addShutdownHook调用,观察是否能秒退 —— 如果可以,问题就定位到某一个钩子
排查时容易漏掉的细节
有些行为看似“程序结束了”,实则触发了隐式后台机制:
-
javax.management.MBeanServer或com.sun.jdmk.comm.HtmlAdaptorServer:JMX 相关服务未关闭,会启动监听线程 -
sun.misc.URLClassPath缓存中残留jar:file://URL,导致URLJarFile持有FileInputStream,进而阻止 JVM 退出(尤其在 Windows 上) - 使用了
Unsafe.park()或LockSupport.park()且未配对unpark()的自定义同步工具 - 某些测试框架(如 JUnit 5 的
@TempDir)会在幕后启动清理线程,若测试类没被 GC,线程可能残留
真正麻烦的不是“找不到线程”,而是那些名字普通、归属不明的线程 —— 它们往往来自某一行你没意识到会起后台线程的第三方调用。这时候得结合 jstack 输出里的线程栈,一层层回溯到你的代码调用点,而不是只看线程名猜。











