Examine .Net Memory Leaks

Writing programs using .Net is very productive. One reason is because much of memory management is “managed” for you. In C, C++ and other “native” languages, if you allocate memory, you’re responsible for freeing it. There were stopgap measures, like destructors, SmartPointers and reference counting, which helped, but were still cumbersome.

Foxpro manages memory for you, and has used garbage collection for decades: Heartbeat: Garbage collection in VFP and .NET are similar

However, you can still have memory leaks in .Net.

Try this in VS 2008: (I think it will work in 2005 too, can somebody verify? Thanks)

1. File->New->Project->VB Windows Console Application.

2. Paste the sample code below.

3. Project->Properties->Debug->Enable Unmanaged Code debugging

If you’re running 64 bit, you can force 32 bit targeting: Project->Properties->Compile->Advanced Compile Settings->Target CPU->x86 (see this for more about x64)

The sample code has a loop that creates and releases an instance of a class MyWatcher that uses the FileSystemWatcher class that reacts to events, such as files being created in a directory.

The class has a member (Dim MyLargeMemoryEater(100000) As String) which eats up 4 bytes (8 bytes on x64) per array element. At the end of each loop, the garbage collector is called to release everything. The class has a Finalize method that will be called when the garbage collector collects.

When you run the code, you see the increase in memory use in each loop. The increase per iteration is just a little more than the amount of memory used by MyLargeMemoryEater . Also, the Finalizers don’t run until the application is shutting down.

If you uncomment the “UnSubscribe” line, then the finalizer fires (on a different thread) and you can see the leak is gone.

Now let’s examine the leak by looking at the heap.

Make it leak, then put a breakpoint on the “Done” line after the loop. At this point, we suspect there are 100 instances of MyWatcher in the managed heap.

Wouldn’t it be great to see them? Let’s use SOS:

Open the Immediate window: Debug Menu->Windows-> Immediate window

Type in the lines in red

!load sos.dll

extension C:\Windows\Microsoft.NET\Framework\v2.0.50727\sos.dll loaded

!dumpheap -type MyWatcher

PDB symbol for mscorwks.dll not loaded

 Address MT Size

02b7431c 000d313c 16

<…>

02bdc3f0 000d313c 16

02bdc550 000d313c 16

total 100 objects

Statistics:

      MT Count TotalSize Class Name

000d313c 100 1600 ConsoleApplication1.MyWatcher

Total 100 objects

So now we know that there are 100 instances still around. Why were they not garbage collected? Because somebody has a reference to them. Choose one of the instances (02bdc550), and use the gcroot command:

!gcroot 02bdc550

Note: Roots found on stacks may be false positives. Run "!help gcroot" for

more info.

Error during command: Warning. Extension is using a callback which Visual Studio does not implement.

Scan Thread 6948 OSTHread 1b24

Scan Thread 536 OSTHread 218

Scan Thread 6448 OSTHread 1930

DOMAIN(00601068):HANDLE(AsyncPinned):b15b4:Root:02bdda44(System.Threading.OverlappedData)->

02bddf38(System.Threading.IOCompletionCallback)->

02bdcfc4(System.IO.FileSystemWatcher)->

02b8b14c(System.IO.FileSystemEventHandler)->

02bdc550(ConsoleApplication1.MyWatcher)

Now we see that the FileSystemWatcher is referencing us via an eventhandler.

(The “!dumpheap -stat” command is also very useful to see what’s on the heap)

See also:

Collecting garbage at the wrong time

SOS Debugging Extension (SOS.dll)

https://www.codeproject.com/KB/dotnet/Memory_Leak_Detection.aspx

https://www.julmar.com/blog/mark/PermaLink,guid,643649fc-0467-4f0d-9a95-323ed7ce4298.aspx

Debugging a memory leak in managed code: Ping - SendAsync

<Code Sample>

Module Module1

    Friend g_cnt As Integer

    Sub Main()

        'uncomment these 2 lines to test the event watcher

        'Dim oFileWatcher = New MyWatcher

        'MsgBox("Wait in msgbox. FSW events still fire: copy a file into d:\")

        Dim oldPeak = 0L

        For i = 1 To 100

            Dim oWatcher = New MyWatcher

            ' oWatcher.UnSubscribe() ' uncomment this line to remove handler

            oWatcher = Nothing

            GC.Collect() ' collect garbage

            GC.WaitForPendingFinalizers() ' allow finalizers

            GC.Collect() ' collect again for any objects that had finalizers

            Dim newpeak = Process.GetCurrentProcess.PeakWorkingSet64

            Debug.WriteLine("All released? " + i.ToString + " " + _

                    " WorkingSet =" + Process.GetCurrentProcess.WorkingSet64.ToString("n0") + _

                    " Peak=" + newpeak.ToString("n0") + _

                    " delta =" + (newpeak - oldPeak).ToString)

            oldPeak = newpeak

        Next

        GC.Collect() ' collect garbage

        GC.WaitForPendingFinalizers() ' allow finalizers

        GC.Collect() ' collect again for any objects that had finalizers

        Debug.WriteLine("Done")

    End Sub

End Module

Class MyWatcher

    Dim MyLargeMemoryEater(100000) As String ' make the instance bigger to magnify issue: 4 bytes per array item on x86

    Dim fsw As IO.FileSystemWatcher

    Sub New()

        fsw = New IO.FileSystemWatcher

        fsw.Path = "d:\"

        fsw.Filter = "*.*"

        AddHandler fsw.Created, AddressOf OnWatcherFileCreated

        fsw.EnableRaisingEvents = True

    End Sub

    Sub UnSubscribe()

        RemoveHandler fsw.Created, AddressOf OnWatcherFileCreated

    End Sub

    Sub OnWatcherFileCreated(ByVal sender As Object, ByVal args As System.IO.FileSystemEventArgs)

        Debug.WriteLine((New StackTrace).GetFrames(0).GetMethod.Name + " " + args.FullPath)

    End Sub

    Protected Overrides Sub Finalize() ' called when garbage collector collects on the GC Finalizer thread.

        MyBase.Finalize()

        Debug.WriteLine((New StackTrace).GetFrames(0).GetMethod.Name + g_cnt.ToString + " Thread= " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString)

    End Sub

End Class

</Code Sample>