Friday, August 16, 2013

Tip: find memory leaks in .net application

Sometimes we face some nasty problems, while working with managed code. And one of them is memory leak. When you see in task manager, that an application slowly (or rapidly) consumes precious memory, it means that garbage collector can't release some resources, allocated in code. Sometimes it is easy to find the weak spot, sometimes it is not. Memory leak can be represented by something like holding the reference to managed object longer, than it's needed, or not releasing unmanaged resources.

Those problems are tightly connected with managed heap, so basically we can look inside the heap and find, what is consuming memory. To check what contains in heap we can use SOS Debugging Extension for visual studio.

So lets say we have a code like this somewhere deep inside the application:

class Program
{
 public static event EventHandler UserLogin;

 private static void Main()
 {
  for(int i=0; ; i++)
  {
   var user = new User();
  }
  Console.ReadLine();
 }
 public class User
 {
  public User()
  {
   Program.UserLogin += OnUserLogin;
  }

  private void OnUserLogin(object sender, EventArgs eventArg) { }
 }
}

First of all we need to enable debugging unmanaged/native code in debug tab in project properties. After that run the application, let it work for a while, go to the Debug tab and select Break All. Now open Immediate Window (ctrl+alt+i) and type

.load sos

SOS is loaded and we can analyze the heap now using command

!dumpheap -stat

Don't forget the -stat postfix, if you don't want to see all the heap itself.

Now the immediate window probably looks like:

.load sos
extension C:\Windows\Microsoft.NET\Framework\v4.0.30319\sos.dll loaded
!dumpheap -stat
PDB symbol for clr.dll not loaded
Statistics:
      MT    Count    TotalSize Class Name
6555a87c        1           12 System.Collections.Generic.ObjectEqualityComparer`1[[System.Type, mscorlib]]
6555e1b0        1           16 System.Security.Policy.AssemblyEvidenceFactory
6555e0fc        1           20 Microsoft.Win32.SafeHandles.SafePEFileHandle
6555b350        2           24 System.Object
6555ce10        1           28 System.Reflection.RuntimeAssembly
6555b3a4        1           28 System.SharedStatics
6555e158        1           32 System.Security.Policy.PEFileEvidenceFactory
6555bc28        1           36 System.Security.PermissionSet
6555baa0        1           40 System.Security.Policy.Evidence
6555ad54        1           44 System.Threading.ReaderWriterLock
65172268        1           48 System.Collections.Generic.Dictionary`2[[System.Type, mscorlib],[System.Security.Policy.EvidenceTypeDescriptor, mscorlib]]
6555b7d4        1           68 System.AppDomainSetup
6555b2d4        1           84 System.ExecutionEngineException
6555b290        1           84 System.StackOverflowException
6555b24c        1           84 System.OutOfMemoryException
6555b0f8        1           84 System.Exception
6555b420        1          112 System.AppDomain
6555b318        2          168 System.Threading.ThreadAbortException
6555ace8        1          284 System.Collections.Generic.Dictionary`2+Entry[[System.Type, mscorlib],[System.Security.Policy.EvidenceTypeDescriptor, mscorlib]][]
6555c738        2          380 System.Int32[]
6555c168       20          560 System.RuntimeType
6555afb0       32         1580 System.String
0063ee18    82787      2483502      Free
6550ae88        8     50348992 System.Object[]
0014386c  6626825     79521900 MemoryLeak.Program+User
655508a0  6668891    213404512 System.EventHandler
Total 13378586 objects

There is definitely something wrong with the last two records in the heap, we got too much instances of some class and some event handler, and they are occupying a lot of memory. So now we have two method table addresses (0014386c,655508a0) and we know there are problem there.

Now we can take the address (for example 655508a0) and use

!dumpheap -mt 655508a0

to list the objects that correspond to this particular method table address (and since we have 6668891 it will take a while :))

In this output you need any of the addresses in the first column.

We want to see what else in the code related to this objects, so we take any address (for example 025eb870) and use command to display the information about references to an object at this address:

!gcroot 025eb870

and we see something like:

!gcroot 025eb870
HandleTable:
    000f13ec (pinned handle)
    -> 03513330 System.Object[]
    -> 025ebc6c System.EventHandler
    -> 025ba3e8 System.Object[]
    -> 025eb870 System.EventHandler

Not a very good example, but anyway, lets try to find what method contains the problem. Using the command to display information about the object at the specific address:

!do 025eb870

The output will look like this:

!do 025eb870
Name:        System.EventHandler
MethodTable: 655508a0
EEClass:     651637c0
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
6555b350  400002d        4        System.Object  0 instance 025eb864 _target
6555b350  400002e        8        System.Object  0 instance 00000000 _methodBase
6555a3d4  400002f        c        System.IntPtr  1 instance 0011C050 _methodPtr
6555a3d4  4000030       10        System.IntPtr  1 instance 00000000 _methodPtrAux
6555b350  4000031       14        System.Object  0 instance 00000000 _invocationList
6555a3d4  4000032       18        System.IntPtr  1 instance 00000000 _invocationCount

lets once again use !do command for the _target address:

!do 025eb864
Name:        MemoryLeak.Program+User
MethodTable: 0011386c
EEClass:     001112f0
Size:        12(0xc) bytes
File:        D:\work\test\MemoryLeak\MemoryLeak\bin\Debug\MemoryLeak.exe
Fields:
None

We found the class with the problem, lets look inside this class and list its method table:

!dumpmt -md 0011386c

Now we see an event (0011C050 0011385C NONE MemoryLeak.Program+User.OnUserLogin(System.Object, System.EventArgs)) with entry address 0011C050 matching the _methodPtr in the event in table before previous one.

!dumpmt -md 0011386c
EEClass:         001112f0
Module:          00112e94
Name:            MemoryLeak.Program+User
mdToken:         02000003
File:            D:\work\test\MemoryLeak\MemoryLeak\bin\Debug\MemoryLeak.exe
BaseSize:        0xc
ComponentSize:   0x0
Slots in VTable: 6
Number of IFaces in IFaceMap: 0
--------------------------------------
MethodDesc Table
   Entry MethodDe    JIT Name
65464960 65166728 PreJIT System.Object.ToString()
65458790 65166730 PreJIT System.Object.Equals(System.Object)
65458360 65166750 PreJIT System.Object.GetHashCode()
654516F0 65166764 PreJIT System.Object.Finalize()
0011C048 00113850    JIT MemoryLeak.Program+User..ctor()
0011C050 0011385C   NONE MemoryLeak.Program+User.OnUserLogin(System.Object, System.EventArgs)
So here we are: found a possible memory leak source, and the later code review hopefully will help to fix it.

No comments :

Post a Comment