一、背景C#程序内存泄露的原因有很多,但是从顶层原理来看,应该销毁的用户root对象没有被销毁,导致意外的外部对象无限堆积,导致内存飙升并最终崩溃。用户根之一是终结器队列。在本文中,我们将了解如何将PerfView与WinDbg相结合。2.Howtogoinsight1.Finalizermemoryleak为了模拟finalizer内存泄漏,我们故意在析构函数中执行复杂的逻辑,让析构函数进程足够慢,使得分配速度远大于销毁速度,从而实现容量不足导致消耗内存激增,参考如下代码:internalclassProgram{staticvoidMain(string[]args){Task.Run(Add);控制台.ReadLine();}staticvoidAdd(){for(inti=0;i<1000000;i++){varperson=newPerson(){Name=$"jack{i}",Age=i};}}}publicclassPerson{publicstringName{get;放;}publicintAge{得到;放;}~Person(){Thread.Sleep(newRandom().Next(0,3000));Console.WriteLine($"name={Name}已被销毁...");}}分配操作完成后,使用WinDbg附加到进程,使用!fq查看内存状态,输出如下:0:015>!fqSyncBlockstobecleanedup:0Free-ThreadedInterfacestobe释放:0MTA释放接口:0STA释放接口:0--------------------------------------第0代有28423个可终结对象(000000001BF5B108->000000001BF92940)generation1has4finalizableobjects(000000001BF5B0E8->000000001BF5B108)generation2has21finalizableobjects(000000001BF5B040->000000001BF5B0E8)Readyforfinalization971560objects(000000001BF92940->000000001C6FC280)Statisticsforallfinalizableobjects(includingallobjectsreadyforfinalization):MTCountTotalSizeClassName00007ffdbaa2f410496System.WeakReference00007ffdbaa4f3682112System.Threading.ThreadPoolWorkQueueThreadLocals00007ffdbaa4c6401168System.Diagnostics.Tracing.FrameworkEventSource00007ffdbaa417b81168System.Diagnostics.Tracing.NativeRuntimeEventSource00007ffdbaa4a1581176System.Threading.Tasks.TplEventSource00007ffdbaa056503216System.Threading.Thread00007ffdbaa240a81320System.Diagnostics.Tracing.RuntimeEventSource00007ffdbaa24ac88896System.Diagnostics.Tracing.EventSource+OverideEventProvider00007ffdbaa4fb5899998731999584ConsoleApp2.PersonTotal1000008个对象从上面Readyforfinalization971560个对象可以看出,目前正在排队等待Finalizer对象执行。可以执行,为什么执行这么慢?这个时候就要调查一下FinalizerThread此时在做什么0:001>!tThreadCount:7UnstartedThread:0BackgroundThread:6PendingThread:0DeadThread:0HostedRuntime:noLockDBGIDOSIDThreadOBJStateGCModeGCAllocContextDomainCountAptException014f98000000000058C2702a020Preemptive0000000000000000:000000000000000000000000005814e01MTA523f4c000000001AA94090202b220Preemptive00000000088A9D60:00000000088A9FD000000000005814e00MTA(Finalizer)7324b8000000001AA986D0102a220Preemptive0000000000000000:000000000000000000000000005814e00MTA(ThreadpoolWorker)1145520000000000056F5801029220Preemptive00000000088A5100:00000000088A5FD000000000005814e00MTA(ThreadpoolWorker)1251004000000001AB261601029220Preemptive0000000000000000:000000000000000000000000005814e00MTA(ThreadpoolWorker)13658a8000000001B6D35D021220Preemptive0000000000000000:000000000000000000000000005814e00Ukn1475b8000000001B6508201029220Preemptive0000000000000000:000000000000000000000000005814e00MTA(ThreadpoolWorker)0:001>~~[3f4c]sntdll!NtDelayExecution+0x14:00007ffe`8908c634c3ret0:005>!clrstackOSThreadId:0x3f4c(5)ChildSPIPCallSite000000001ACEF86800007ffe8908c634[HelperMethodFrame:000000001acef868]System.Threading.Thread.SleepInternal(Int32)000000001ACEF96000007ffe19f0c46bSystem.Threading.Thread.睡眠(Int32)[/_/src/System.Private.CoreLib/src/System/Threading/Thread.CoreCLR.cs@259]000000001ACEF99000007ffdba986e15ConsoleApp2.Person.Finalize()[D:\net6\ConsoleApp1\ConsoleApp2\Program.cs@31]000000001ACEFCE000007ffe1a4a6c06[DebuggerU2MCatchHandlerFrame:000000001acefce0]从输出中可您可以看到终结器线程正在运行Sleep()函数。如果有源码可以看ConsoleApp2.Person.Finalize()中具体的业务逻辑。如果没有源码,可以使用!U00007ffdba986e15反汇编方法源码0:005>!U00007ffdba986e15NormalJITgeneratedcodeConsoleApp2.Person.Finalize()ilAddris00000000023920E0pImportis0000000002FFF460Begin00007FFDBA986DA0,sizee9D:\net6\ConsoleApp1\ConsoleApp2\Program.cs@31:00007ffd`ba986dd448b998b4a6bafd7f0000movrcx,7FFDBAA6B498h(MT:System.Random)00007ffd`ba986ddee85d0ab25fcallcoreclr!JIT_TrialAllocSFastMP_InlineGetThread(00007ffe`1a4a7840)00007ffd`ba986de3488945f8movqwordptr[rbp-8],rax00007ffd`ba986de7488b4df8movrcx,qwordptr[rbp-8]00007ffd`ba986debe848fdffffcall00007ffd`ba986b38(System.Random..ctor(),mdToken:00000000060015AB)00007ffd`ba986df0488b4df8movrcx,qwordptr[rbp-8]00007ffd`ba986df433d2xoredx,edx00007ffd`ba986df641??b8b80b0000movr8d,0BB8h00007ffd`ba986dfc488b45f8movrax,qwordptr[rbp-8]00007ffd`ba986e00488b00movrax,qwordptr[rax]00007ffd`ba986e03488b4040movrax,qwordptr[rax+40h]00007ffd`ba986e07ff5030callqwordptr[rax+30h]00007ffd`ba986e0a8945f4movdwordptr[rbp-0Ch],eax00007ffd`ba986e0d8b4df4movecx,dbp0ffd-0Ctr][`ba986e10e833e7feff调用00007ffd`ba975548(system.threading.thread.thread.sleep(int32),mdtoken:0000000006001cd5)RCX,QWORDPTR[125D30F0H](“名称=”)00007FFD`ba986e1e48894De8MOVQWORDPTRPTR[RBP-18H],RCX0000007FABA986E22488B4D10MOVRCX,QWORSRCX,QWORDPRCRPTR[rbp+10ff.rbp+10ff[rbp+10hf]rbp+10hd.rbp+10h]rbp+10h]。ptr[rbp-18h]00007ffd`ba986e3b488b55e0movrdx,qwordptr[rbp-20h]00007ffd`ba986e3fe864d7feffcall00007ffd`ba9745a8(System.String.Concat(System.String,System.String,System.String),mdToken:0000000006000705)00007ffd`ba986e44488945d8movqwordptr[rbp-28h],rax00007ffd`ba986e48488b4dd8movrcx,qwordptr[rbp-28h]00007ffd`ba986e4ce8cf99ffffcall00007ffd`ba980820(System.Console.WriteLine(System.String),mdToken:0000000006000081)00007ffd`ba986e5190nop00007ffd`ba986e5290nop00007ffd`ba986e53eb00jmp00007ffd`ba986e5500007ffd`ba986e55488bccmovrcx,rsp00007ffd`ba986e58e808000000call00007ffd`ba986e65(ConsoleApp2.Person.Finalize(),mdToken:0000000006000008)00007ffd`ba986e5d90nopD:\net6\ConsoleApp1\ConsoleApp2\Program.cs@33:00007ffd`ba986e5e90nop00007ffd`ba986e5f488d6500learsp,[rbp]00007ffd`ba986e635dpoprbp00007ffd`ba986e64c3ret最终找到了问题的原因。在实际项目中,肯定没有那么简单,往往会执行一个复杂的逻辑。接下来,我们有一个好奇的点,那个复杂的逻辑要执行多长时间?因为dump只是一个静态快照,所以从dump中查找的方式被屏蔽了。有什么解决办法吗?一定有,让PerfView大威天龙2.Finalize()有多慢?在CoreCLR中,有一些监视FinalizerThread线程的ETW事件,具体为:1)FinalizersStart事件2)FinalizerObject事件3)FinalizersStop事件当一个对象准备好析构时,会触发FinalizerObjectETW事件,所以通过观察析构间隔对象之间,大概可以看出大概耗时。了解原理后,打开PerfView,使用默认设置,启用Collect->Collectcollection,然后运行应用程序。运行一段时间后,点击StopCollection,在生成的zip面板中点击Event,搜索Finalizekey截图如下:从图中可以看出,TypeName列是一个Person对象,从TimeMSec时间戳,可以观察到Person和Person之间的距离超过了s级别,这至少说明析构函数的执行速度是真的快。慢的。
