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>