C# 程序内存泄漏的诱发因素有很多,但从顶层原理上来说,就是该销毁的 用户根 对象没有被销毁,从而导致内存中意料之外的对象无限堆积,导致内存暴涨,最终崩溃,这其中的一个用户根就是 终结器队列,这一篇我们就来看下如何让 PerfView 配合 WinDbg 双剑合璧。,1. 终结器内存泄漏,为了模拟终结器内存泄漏,我们故意在析构函数中执行复杂的逻辑,让析构过程足够的慢,这样可以实现 分配速度远大于销毁速度 ,达到消费能力不足引发的内存暴涨, 参考如下代码:,当分配操作结束后,用 WinDbg 附加到进程中,使用 !fq 查看内存情况,输出如下:,从上面的 Ready for finalization 971560 objects 中可以看到,当前有 9.7w 的对象正在排队等待 Finalizer 线程执行,既然它可以执行,为什么执行这么慢呢?这时候就需要调查下 Finalizer Thread 此时正在干嘛。,从输出中可以看到,终结器线程正在 Sleep() 函数,如果你有源码的话,可以看下 ConsoleApp2.Person.Finalize() 中的具体业务逻辑,如果没有源码的话,可以使用 !U 00007ffdba986e15 反汇编下方法源码。,最终我们找到了问题原因,在真实项目中肯定不会这么简单的,往往会执行一个复杂的逻辑,接下来我们就有一个好奇点了,那个 复杂的逻辑 会大概执行多久呢?,因为 dump 只是一个静态快照,所以从 dump 中寻找的路子就封死了,那有没有方案呢?肯定有啦,让 PerfView 大威天龙。,2. Finalize() 到底有多慢,在 CoreCLR 中有一些监控 Finalizer Thread 线程的 ETW 事件,具体是:,1)FinalizersStart 事件 2)FinalizerObject 事件 3)FinalizersStop 事件,当一个对象准备析构时,会触发 FinalizerObject ETW事件,所以观察对象之间的析构间隔,大概就能看出大致的 耗费时间。,知道原理之后,接下来打开 PerfView,使用默认设置,启用 Collect -> Collect 收集,然后把应用程序跑起来,运行一段时间后,点击 Stop Collection ,在生成的 zip 面板中点击 Event ,搜索 Finalize 关键词,截图如下:,
,从图中可以看到,TypeName 列都是 Person 对象,而且从 Time MSec 时间戳上可以观察到 Person 和 Person 之间相隔 s 级以上,起码说明析构函数 执行真的很慢。
© 版权声明
文章版权归作者所有,未经允许请勿转载。