Introduction
If you're doing managed debugging with WinDbg, I believe you should be very familiar with such reference relationships from the !gcroot
command output.
07b3ebd4(System.EventHandler)->
0375891c(SomeWPFApp.MainWindow)->
The object MainWindow
in SomeWPFApp
has many events, what exact method is handing what exact event
obtained by the object 07b3ebd4(System.EventHandler)
?
I'll show you how to use WinDbg to get the answer.
Background
I often help customers find their application performance issues with WinDbg and
the SQL Server Profiler. Recently, I did some research on WPF and SilverLight performance issues.
In most cases, the developer forgot to de-reference the EventHandler
from an event. Thus the most important thing is to find which method is handling which event?
Then, we can simply use -=
to remove the relationship in some method, usually
the Dispose
method.
Step by step
This is a live debug instead of a post-mortem debugging, which means I'm
using WinDbg to attach the process I want to research.
Load SOS
First, this is a .NET application, so I should load the SOS extensions first. For different .NET framework versions, we have to use different load
grammars.
- For .NET 1.0 and .NET 1.1, simply type .load clr10\sos.dll
- For .NET 2.0, .NET 3.0, and .NET 3.5, type .loadby sos mscorwks or .loadby sos mscorsvr
- For .NET 4.0 or higher, type .loadby sos clr
Then, how to find the .NET Framework version? I like using the lmvm command.
0:016> lmvm mscorwks
start end module name
Ooh, nothing. May I'll try the other DLL.
0:016> lmvm clr
start end module name
79140000 797ae000 clr (deferred)
Image path: C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\clr.dll
Image name: clr.dll
----------------------Skip--------------------------------
OK, this is a .NET 4.0 application. So, I'll load the SOS by the corresponding .NET
Framework path.
0:016> .loadby sos clr
Retrieve objects queued in the Finalizable queue
It's a best practice to not implement the "~ClassName" method if you do not use native resources, such as
a Win32 API call
or a managed wrapper for native objects (DirectoryEntry
,
FileStream
, GDI+ objects, etc.).
Even if you create an empty destructor method (named Finalize
in the .NET world), the CLR
will manage such kinds of objects' life cycle in a different way.
OK, let's go back to this article. In this demo, I'll find something from the finalize queue.
0:016> !finalizequeue
SyncBlocks to be cleaned up: 0
MTA Interfaces to be released: 0
STA Interfaces to be released: 0
----------------------------------
generation 0 has 260 finalizable objects (0bfe14f4->0bfe1904)
generation 1 has 15 finalizable objects (0bfe14b8->0bfe14f4)
generation 2 has 6442 finalizable objects (0bfdb010->0bfe14b8)
Ready for finalization 0 objects (0bfe1904->0bfe1904)
Statistics for all finalizable objects (including all objects ready for finalization):
--------------SKIP----------------
79b99fbc 362353 5797648 System.WeakReference
--------------SKIP----------------
I'm interested in the System.WeakReference
object. There're 362353 such objects in memory until now, it's crazy!
Why so much WeakReference
objects? Why does GC not free them?
For question 1, usually allocates too much and frees too less. For question 2, usually something has the reference on this object, and such "something"
has references to the ROOT object, such as the CPU register, static variables.
Let's dump the objects from the managed heap.
0:017> !dumpheap -mt 79b99fbc
Address MT Size
----------Skip many objects----------------
04410400 79b99fbc 16
04410454 79b99fbc 16
044104a8 79b99fbc 16
044104fc 79b99fbc 16
04410550 79b99fbc 16
044105c4 79b99fbc 16
04410618 79b99fbc 16
----------Skip many objects----------------
And then randomly select an object, I selected "044104fc" here.
What's the reference relationship on this object?
We can find the reference path by using the SOS extension command "!gcroot
". Note, some times the command will show nothing to you.
0:000> !gcroot 044104fc
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 0 OSTHread 888
---------Skip too many root information-----------------------
ESP:12e9dc:Root: 0dbc9030(System.Windows.Threading.DispatcherOperation)->
0dbc9010(System.Windows.Threading.DispatcherOperationCallback)->
07b3eb98(System.Windows.Threading.DispatcherTimer)->
07b3ebd4(System.EventHandler)->
0375891c(SomeWPFApp.MainWindow)->
079e7a94(SomeWPFApp.PermissionListBox)->
07987f54(System.Windows.Controls.ControlTemplate)->
079884cc(System.Windows.TemplateContent)->
03700cf0(System.Windows.Baml2006.Baml2006SchemaContext)->
03700ea4(System.Windows.Baml2006.WpfSharedBamlSchemaContext)->
04409188(System.Collections.Concurrent.ConcurrentDictionary`2[[System.Reflection.Assembly, mscorlib],[System.Xaml.MS.Impl.XmlNsInfo, System.Xaml]])->
04411314(System.Object[])->
04411650(System.Collections.Concurrent.ConcurrentDictionary`2+Node[[System.Reflection.Assembly, mscorlib],[System.Xaml.MS.Impl.XmlNsInfo, System.Xaml]])->
04411578(System.Collections.Concurrent.ConcurrentDictionary`2+Node[[System.Reflection.Assembly, mscorlib],[System.Xaml.MS.Impl.XmlNsInfo, System.Xaml]])->
04411530(System.Collections.Concurrent.ConcurrentDictionary`2+Node[[System.Reflection.Assembly, mscorlib],[System.Xaml.MS.Impl.XmlNsInfo, System.Xaml]])->
044104d0(System.Xaml.MS.Impl.XmlNsInfo)->
044104fc(System.WeakReference)
---------Skip too many root information-----------------------
Command cancelled at the user's request.
OK, from the output bottom, we find our lucky baby, 044104fc. This object is held by an
XmlNsInfo
, and such an XmlNsInfo
is held by a generic dictionary, and so on. Then, a SomeWPFApp.MainWindow
instance is held
by a System.EventHandler
.
Why did the EventHandler
have a reference to MainWindow
? What's the
EventHandler
? What's the exact method name in the MainWindow
that is handling such an event?
Dump the method name
The key point is the EventHandler
object.
0:000> !do 07b3ebd4
Name: System.EventHandler
MethodTable: 79b92a30
EEClass: 79882d28
Size: 32(0x20) bytes
File: C:\WINDOWS\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
MT Field Offset Type VT Attr Value Name
79b9f744 4000076 4 System.Object 0 instance 0375891c _target
79b9f744 4000077 8 System.Object 0 instance 00000000 _methodBase
79b9ab88 4000078 c System.IntPtr 1 instance 7454518 _methodPtr
79b9ab88 4000079 10 System.IntPtr 1 instance 0 _methodPtrAux
79b9f744 400007a 14 System.Object 0 instance 00000000 _invocationList
79b9ab88 400007b 18 System.IntPtr 1 instance 0 _invocationCount
In fact, the first bold target address is 0375891c; if you scroll up, you'll find this is the
MainWindow
object.
The second _methodPtr
will tell us something. This is an IntPtr
, a pointer to something. The address is 7454518.
Usually, this points to a Method Descriptor. So let's dump the information by a SOS command.
0:000> !ip2md 7454518
Failed to request MethodData, not in JIT code range
What's this? This means the method may not be JITted yet. Or, this is the entry point to a real method. Let's check the disassemble code.
0:000> !u 7454518
Unmanaged code
07454518 e93336a703 jmp 0aec7b50
0745451d 5f pop edi
0745451e 0000 add byte ptr [eax],al
07454520 c45dc6 les ebx,fword ptr [ebp-3Ah]
07454523 0500000000 add eax,0
07454528 e8bbddce71 call clr!PrecodeFixupThunk (791422e8)
0745452d 5e pop esi
0745452e 0000 add byte ptr [eax],al
07454530 58 pop eax
07454531 5d pop ebp
The first line is a jmp
, it jumps to address 0aec7b50. In fact, this address is pointed to a real managed method.
We can use !ip2md
to dump the descriptor information.
0:000> !ip2md 0aec7b50
MethodDesc: 05c65dc4
Method Name: SomeWPFApp.MainWindow.dispatcherTimer_Tick(System.Object, System.EventArgs)
Class: 06117abc
MethodTable: 05c6689c
mdToken: 06000054
Module: 03572ea4
IsJitted: yes
CodeAddr: 0aec7b50
Transparency: Critical
*** WARNING: Unable to verify checksum for C:\SomeFolder\SomeWPFApp.
Source file: E:\work\SomeFolder\SomeWPFApp\MainWindow.xaml. @ 1300
OK, we get the method name now. In class MainWindow
, we have a method named
dispatcherTimer_Tick
, this method handles the event Tick
of a
DispatcherTimer
object.
The developer should use "-=" to remove this event handler at some place in the code, such as
the Closing
event or Dispose
method. Then when the GC begins work, these objects
can be collected.
Notice, you'll not always find the method descriptor via this method. If the code is not
JITted yet, this method doesn't work.
You should check the managed heap by dd <address> and try the third double
word value by using !dumpmd <3rd dd address>
. You can reference
this article written by Mark, http://julmar.com/blog/mark/?p=79.