System.Threading.Timer引发Windows服务内存泄漏问题排查求助
结合你提供的Windbg输出和代码,咱们一步步拆解这个内存泄漏问题:
一、Windbg输出里的关键线索
从你贴的!gcroot结果来看,那个超大的System.Object[]被System.Net.TimerThread.ThreadProc持有——这个TimerThread是.NET内部用来管理System.Threading.Timer的线程池线程,意味着和Timer相关的对象链被长期挂住,没法被GC回收。再看!dumpheap的统计,那个268MB的Object[]绝对是内存暴涨的核心元凶,你可以用!dumparray 10ee1010命令查看它的具体内容,能更精准定位里面装的是什么对象。
二、代码里的潜在泄漏点
看你的简化代码,我发现几个大概率的问题:
事件订阅只加不减
每次Timer触发时,你都会创建新的QueryProcessor实例,然后执行search.resultFoundEvent += QueryResultEventListener——但从来没执行过-=移除订阅!这就导致每个QueryProcessor实例都会持有对QueryResultEventListener所在类实例的强引用(因为QueryResultEventListener是实例方法),而你的服务类里还持有静态Timert,这一整条引用链都会被死死拽住,GC根本没法回收这些QueryProcessor,日积月累内存肯定会爆。Timer的引用连锁反应
静态Timert本身会被AppDomain长期持有,这本身没问题,但如果你的服务类实例因为事件订阅被大量QueryProcessor引用,就会导致整个服务实例都没法被回收,连带里面的所有资源都占着内存。
三、具体修复方案
针对这些问题,给你几个落地的修复步骤:
必须移除事件订阅
如果RunSearch是同步执行的,那在执行完后立刻移除订阅:foreach (var query in queries) { var search = new QueryProcessor(query); search.resultFoundEvent += QueryResultEventListener; try { search.RunSearch(); } finally { // 关键:用完就取消订阅,切断引用链 search.resultFoundEvent -= QueryResultEventListener; } }如果
RunSearch是异步的(事件会在后续触发),那得在事件处理完成后移除订阅,或者改用弱事件模式(比如WeakEventManager),避免强引用导致的泄漏。排查超大数组的内容
执行!dumparray 10ee1010看看这个大数组里到底存的是QueryProcessor实例、JobDetails还是其他对象,这样能直接锁定泄漏的根源。规范对象生命周期管理
如果QueryProcessor实现了IDisposable接口,一定要在Dispose方法里取消事件订阅,然后用using语句包裹使用,确保资源被正确释放:foreach (var query in queries) { using (var search = new QueryProcessor(query)) { search.resultFoundEvent += QueryResultEventListener; search.RunSearch(); // 异步场景下可能需要在事件回调里取消订阅并Dispose } }
四、额外排查方向
- 检查
JobDetails对象是否持有大量数据,或者被其他长期存在的对象(比如静态缓存)引用,导致无法被GC回收。 - 用
!dumpheap -mt 73a20cbc列出所有Object[]实例,找出除了那个超大数组外的其他大数组,再用!gcroot查看它们的引用链,确认是否还有其他泄漏点。
内容的提问来源于stack exchange,提问作者propagated

