虚拟线程异常终止后栈帧会被JVM立即卸载,所谓“未销毁”实为任务对象被静态集合、缓存、未关闭流或结构化并发作用域未正确关闭等强引用持有,导致关联对象无法GC,引发内存缓慢增长;排查应聚焦堆内存与引用链,而非栈帧本身。

异步子线程异常终止后虚拟机栈帧未彻底销毁,本质不是“栈帧残留”,而是虚拟线程或平台线程因未捕获异常退出时,其关联的栈帧虽被JVM自动回收,但若存在强引用持有、结构化并发作用域未关闭、或本地变量阻塞GC等情况,会导致对象无法及时释放,间接表现为内存缓慢增长、线程快照中出现大量“已终止但未清理”的痕迹。排查重点不在栈帧本身(它不会长期驻留),而在其背后未释放的资源与上下文。
确认是否真为虚拟线程且处于异常终止状态
Java 19+ 中,虚拟线程异常退出后会立即卸载栈帧,不复用;但若你看到大量“PARKED”“TERMINATED”却仍出现在jstack中,大概率是:
- 该线程实际是平台线程(比如误用
new Thread()而非Thread.ofVirtual()) - 线程虽终止,但其任务对象(如Runnable、Lambda闭包)被外部容器、缓存、监听器等强引用持有着
- 使用了结构化并发(
StructuredTaskScope)但未正确调用close()或未用try-with-resources,导致作用域内所有子任务上下文被隐式保留
检查线程转储中栈帧与关联对象生命周期
运行jstack <pid>或jcmd <pid> Thread.print,重点关注含vthread或virtual标识的线程:
- 若某虚拟线程显示
java.lang.Thread.State: TERMINATED,但其堆栈顶部仍可见业务类名(如MyService.process(...)),说明该线程执行体对象尚未被GC——需检查该对象是否被静态集合、缓存、未关闭的流、未注销的回调等意外持有 - 对平台线程,查看是否有
WAITING on java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject类堆栈残留,这往往意味着锁未释放或await未配对signal
验证结构化并发作用域是否完整退出
虚拟线程常配合StructuredTaskScope使用,一旦异常发生而作用域未显式关闭,其内部维护的子任务元数据、异常聚合容器、甚至子线程引用都可能滞留:
- 确保所有
StructuredTaskScope实例均在try-with-resources中声明:try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { ... } - 避免在scope内抛出异常后直接return,跳过close逻辑;也勿在catch块中仅打印日志就忽略
scope.close() - 可通过JVM参数
-Djdk.traceVirtualThreads=true开启生命周期事件日志,观察是否有VirtualThread terminated但无对应scope closed记录
定位强引用链:用jmap + MAT分析堆快照
当怀疑栈帧“未销毁”实为对象泄漏时,应转向堆内存分析:
- 触发一次Full GC后执行:
jmap -dump:format=b,file=heap.hprof <pid> - 用Eclipse MAT打开,按“Leak Suspects”报告查看高频未释放对象;筛选出你的业务Runnable、Supplier、或自定义任务类
- 右键→“Path to GC Roots”→选择“with all references”,查看谁在强引用它——常见根因包括:静态ExecutorService未shutdown、Spring Bean中持有未清理的ThreadLocal、监听器注册后未反注册

















