团结引擎长时间挂机导致系统卡死的问题排查
这两天用Tuanjie开发项目, 挂机一段时间之后电脑总是莫名其妙的卡死(通常是第二天上班发现),没蓝屏,也没有稳定的报错弹窗, 只偶尔会有几个应用程序报错提示OOM, 一旦出现基本只能长按电源键重启。
最后经过排查, 是Tuanjie内部自己的内存泄漏问题(Unity6主分支上目前也存在), 但通过一些特殊途径可能更高频的触发导致卡死, 在此记录一下排查的过程.

环境
- 操作系统:Windows 11
- 编辑器:Tuanjie
2022.3.62t6 - 构建分支:
tuanjie/1.8/staging
日志
因为出问题的时候系统基本卡死了, 所以除了Tuanjie 自己的日志, 我也看了下系统的崩溃日志, 有没有哪个子进程占用格外的高.
直接让GPT给我扫了一下盘, 一周内共找到 60 条资源耗尽事件,最关键的是 Event ID 2004, 官方文档和信息描述都提示了确实是内存不足导致的崩溃:

有意思的是,这些事件的成因并不完全一样:
- 有一次,Tuanjie 私有内存从约 35.18 GiB 涨到 39.25 GiB,嫌疑很大。
- 另一次,系统提交量达到约 94.93/94.94 GiB。
svchost涨到 50.74 GiB。DoSvc相关进程约 12.65 GiB。- Tuanjie 约 9.08 GiB。
下面是两条典型的 Event ID 2004,字节数我自己加了下分隔,方便复核。
1 | [2026/7/16 11:30:59] |
导出的系统日志里共有 83 条 Event ID 2004,向前筛七天是 60 条。
第一条里,Tuanjie 确实是最大消费者。
第二条很不一样,真正吃掉大头的是 svchost。
这就有点麻烦了:Tuanjie 有嫌疑,但不是每次 OOM 的唯一答案, 可能最终的崩溃是由多个内存泄漏的Bug导致的. 我们这次的目标是先把影响最大的搞掉, 至少让编辑器不要挂机太久就崩溃
排查工具
让AI搓了个MemoryLeakTracker, 大体功能是当系统内存在一段时间内异常增长时, 采集以下信息:
| 采集内容 | 工具 | 规则 |
|---|---|---|
| 系统内存和内核池 | 性能计数器 | 15 秒一次,循环保存 |
| Tuanjie 内存、句柄和线程 | 性能计数器 | 达到阈值时留档 |
| 句柄、堆和虚拟内存栈 | WPR/ETW | 保存约 10 分钟 ETL |
| 进程现场 | ProcDump | 私有内存到 12 GiB 时转储 |
| 辅助证据 | 事件日志和系统快照 | 事件前后各保存一份 |
满足以下条件之一就会触发这个采集器:
- Tuanjie 私有内存超过 6 GiB
- 系统提交率超过 70%
- 句柄超过 10,000
- 10 分钟内增长超过 768 MiB
- 私有内存达到 12 GiB 时生成转储
这个工具并不解决系统会崩溃重启的问题, 只要死之前能留下遗言就够了 (悲) 这样我下次登录的时候就有希望看到更具体的日志。如果没有 ETL 和转储,只能得到一句“占用很高”这种正确的废话, 然并卵。有了调用栈,才有机会追到 DuplicateHandle 的上层。
问题复现
工具做好之后直接重启挂机了一段时间, 很意外的是才没过几分钟工具就弹窗提示开始记录了. 看来平时崩溃几天才出现一次或许是因为这台电脑有64G的物理内存能硬抗一段时间.
1. 复现
第一份关键采集位于:
E:\MemoryLeak-Capture\Incidents\20260720-142209-PID22848
14:21:19,两个 AssetImportWorker 启动。
14:22:09,其中一个工作进程达到约 11,590 个句柄,自动采集被触发。
自动生成的 incident-20260720-142210.json 中,关键字段如下:
1 | { |
当时系统提交率只有 29.05%,并不是正在发生的 OOM,而是追踪器提前抓到了异常行为。
主进程的句柄变化非常典型, 基本就是一个上升-下降的锯齿状曲线, 然后毫不令人意外的是每次锯齿之后的量会比上次多一些:
| 时间 | 主进程句柄数 |
|---|---|
| 14:22:10 | 6,060 |
| 14:24:18 | 35,684 |
| 14:29:57 | 120,037 |
| 14:30:29 | 8,846 |
| 14:38:51 | 132,168 |
14:59:59 到 15:00:04,只有 5 秒,句柄却从 14,884 涨到 16,825,私有内存仍在约 4 GiB。
这里已经基本可以确定排查方向, 泄露的不是堆内存而是句柄。
2. 分析
接着用 Sysinternals Handle 检查 PID 27308 (刚才出问题的pid)。
一次快照里,数据是这样的:
- 总句柄数:97,998
Thread句柄:92,490- 实际存活线程:289
Handle 工具的关键输出摘录如下:
1 | Handle type summary: |
九万多个 Thread 句柄,鼠实有点吓人。
再按目标线程聚合,超过 99% 的异常句柄都指向 TID 30960, 也就是说这一坨句柄其实都是这一个线程搞出来的, 实际上系统并没有开启那么多任务.
高度怀疑是这个30960背后的服务被复制或重启了N次, 导致句柄数量一直在增长.
3. 石锤
为了解析 ETL,安装了 Windows Performance Toolkit, WPAExporter 加载了微软符号和 Tuanjie 符号。
只看 15 秒,同一条复制栈就出现了 219,368 次,这个数字已经离谱了。
1 | Thread::RunThreadWrapper |
WPA 导出结果的聚合摘要如下:
1 | Trace range : 15 seconds |
到了这里,基本可以说是破案了:
PreviewTextureManager的后台加载线程正在执行预览任务。- 它通过
ProtectedScopedThreadAttach把原生线程附加到 Mono。 - Mono 会复制原生线程句柄。
- 调用点:
mono_threads_open_native_thread_handle
- 调用点:
- 句柄没有被及时关闭,同一个线程对象越积越多。
根因其实还是落在 Tuanjie 引擎内部,位置是预览线程与 Mono 附加路径。
为什么这个项目特别容易触发
“谁在漏”已经找到了,但事情还没完。同一个引擎,为什么这个项目触发得这么频繁?
编辑器日志显示,在异常开始前后,下面这个文件被反复检测、写入和导入:
1 | Assets/Res/Editor/UI/UXTool/Tools/UserDatas/FilesRecentlySelectedData.json |
顺着日志和代码排查,发现其实是ThunderFire UXTool的收藏夹功能引发的频繁写入:
RecentSelectRecord订阅Selection.selectionChanged。- 每次选择改变时,代码会先删除旧记录,再添加新记录。
- 删除和添加都会调用
JsonAssetManager.SaveAssets。 SaveAssets把 JSON 写进Assets目录。- 写入接口:
File.WriteAllText
- 写入接口:
- 写入会触发 Asset Database 刷新。
- “最近选中”界面的资源项还会调用
AssetPreview.GetAssetPreview。 - 这些预览请求最终进入正在泄漏的
PreviewTextureManager。
重启前的 Editor-prev.log 也对上了。下面只截一小段:
1 | Starting new worker id: 0 with log in .../Logs/AssetImportWorker0.log |
prematurely finalized 和句柄骤降能对上。终结器会回收一批对象,但同一条创建路径还在跑。
关键源码位置包括:
RecentSelectRecord.cs:订阅选择变化以及删除、添加记录RecentFilesSetting.cs:每次修改后立即保存 JSONAssetItemMatFile.cs:请求材质预览AssetItemOthersFile.cs:请求其他资源预览SwitchSetting.json:保存“最近选中面板记录”功能开关
把关键代码放在一起,关系就很直观:
1 | // RecentSelectRecord.cs |
注意, UXTool 只是触发器,它会频繁刷新和预览,但本质这个问题还是 Tuanjie 引擎的锅。
官方问题记录
Unity 官方 Issue Tracker 里有一条类似的记录:UUM-141036。
它的原生调用链几乎一样:
1 | PreviewTextureManager::LoadingLoop |
不过,Tuanjie 是独立分支,Unity那边的事情很可能跟我们已经没有关系了. 目标版本有没有修好,还得看实际问题有没有解决。考虑到项目中途升级引擎版本风险很大, 恐怕这个问题要一直陪伴我们很长时间.
解决
解决不了问题, 那只好解决提(chu)出(fa)问题的工具了. 直接把UXTool的“最近选中面板记录”关闭, 然后重启一下团结:

验证
因为已经确认是句柄的问题, 所以重启之后重点排查句柄:
| 时间 | 句柄数 | 私有内存 |
|---|---|---|
| 15:45:42 | 5,513 | 3,629.7 MiB |
| 15:50:57 | 5,811 | 3,702.5 MiB |
| 15:55:57 | 6,100 | 3,711.8 MiB |
| 16:03:10 | 6,075 | 3,719.1 MiB |
启动阶段,句柄最高到 7,564,很快又回落到 5,500~6,100
39 秒采样里,句柄从 5,831 涨到 5,871,速度约为每秒 1.03 个
过了约 7 分钟,句柄又从 6,100 回到 6,075
旧进程会每秒涨几百个,还会冲到十几万,看来我们已经解决了引发问题的大头. 考虑到这是团结自己的问题, 残留有少许的上涨也不令人意外. 能把崩溃的频率降低就算成功.
重启后的 Handle 摘要如下,总量已经回到正常范围。
1 | Handle type summary: |
总结
总结一下问题的前因后果:
- 表面现象:系统多次内存耗尽、桌面组件崩溃或整机卡死。
- 已确认缺陷:
PreviewTextureManager会反复复制线程句柄,句柄释放不及时。 - 项目侧触发器:UXTool“最近选中”会反复写 JSON,还会刷新资源并请求预览。
- 临时解决:关闭该功能、正常退出并重启 Tuanjie。
- 验证结果:句柄增速从每秒 380~400 个降到约 1 个。
- 线程句柄从 92,490 个降到 557 个。
- 原泄漏特征消失。
- 长期解决:等待或验证 Tuanjie 引擎版本修复,不能把项目侧规避当作引擎缺陷已经消失。