Debugging memory usage in managed code using Windbg

Windbg, is there anything it can't do?

CLR Profiler is great for getting an overview of memory allocations and usage for managed applications but it doesn't work for very large applications and can't be attached after the application has been running for a while to determine why after 12 hours memory use spikes. Windbg can be an acceptable alternative when you find yourself looking at an unexpected memory spike and you're curious where all that memory is being allocated.

Start Windbg, attach to the process, and load the sos dll (see previous post).

We start out by getting a summary of our memory allocations with !dumpheap

0:004> !dumpheap -stat total 8952 objects Statistics:
MT Count TotalSize Class Name
7b4779b8 1 12 System.Windows.Forms.OSFeature
7b475ca8 1 12 System.Windows.Forms.FormCollection
7b474f8c 1 12 System.Windows.Forms.Layout.DefaultLayout
7b4749e0 1 12 System.Windows.Forms.ClientUtils+WeakRefCollection
79128f18 1 12 System.Collections.Generic.GenericEqualityComparer`1[[System.Int16, mscorlib]]
79128b94 1 12 System.Collections.Generic.ObjectEqualityComparer`1[[System.IntPtr, mscorlib]]
79127ae4 1 12 System.Collections.Generic.ObjectEqualityComparer`1[[System.Object, mscorlib]]
79114408 1 12 System.Security.Permissions.ReflectionPermission
791142e8 1 12 System.Security.Permissions.FileDialogPermission
----------------- snip ----------------------
790fd4ec 82 1968 System.Version
7ae76b24 208 2496 System.Drawing.KnownColor
791242ec 20 2952 System.Collections.Hashtable+bucket[]
79114bf0 181 5068 System.Security.SecurityElement
791036b0 229 5496 System.Collections.ArrayList
00155a48 50 23424 Free
79124228 306 47780 System.Object[]
790fa3e0 6662 379600 System.String
79124418 9 3156304 System.Byte[] Total 8952 objects

We've got about 3 megs worth of System.Byte[] allocated so let's focus in on this to see why these objects exist.

0:004> !dumpheap -type System.Byte[]
 Address MT Size
01241e5c 79124418 12
0124aaf4 79124418 28
0124ab48 79124418 140
01251178 79124418 172
0125129c 79124418 28
01254f7c 79124418 10148
02246da8 79124418 1048592
02346db8 79124418 1048592
02446dc8 79124418 1048592
total 9 objects
Statistics: MT Count TotalSize Class Name
79124418 9 3156304 System.Byte[]
Total 9 objects

A few small Byte arrays but 3 of them are a meg each so let's find out why the first object is still alive (or if it is alive at all, a GC might not have happened to collect it yet).

0:004> !gcroot 02246da8
Note: Roots found on stacks may be false positives. Run "!help gcroot" for more info.
ebx:Root:012525d8(System.Windows.Forms.Application+ThreadContext)->
01251d58(WindbgDemo.Form1)->
01251edc(System.Collections.ArrayList)->
0125c278(System.Object[])->
02246da8(System.Byte[])
Scan Thread 0 OSTHread 1194
Scan Thread 2 OSTHread 1720

The application has a reference to Form1 which has a reference to an ArrayList which has a reference to my Byte array. If a GC were to happen right now this object would be kept alive.

Instead of checking each Byte array by address individually we can use the .foreach token.

0:004> .foreach (obj {!dumpheap -type System.Byte[] -short}) {.echo obj;!gcroot obj}
01241e5c
Note: Roots found on stacks may be false positives. Run "!help gcroot" for more info.
Scan Thread 0 OSTHread 1194
Scan Thread 2 OSTHread 1720
DOMAIN(00149768):HANDLE(Pinned):3e13fc:Root:02241010(System.Object[])->
01241e5c(System.Byte[])
0124aaf4
Note: Roots found on stacks may be false positives. Run "!help gcroot" for more info.
Scan Thread 0 OSTHread 1194
Scan Thread 2 OSTHread 1720
DOMAIN(00149768):HANDLE(Pinned):3e13fc:Root:02241010(System.Object[])->
0124a1c4(System.Environment+ResourceHelper)->
0124a358(System.Resources.ResourceManager)->
0124a3a0(System.Collections.Hashtable)->
0124a3d8(System.Collections.Hashtable+bucket[])->
0124a9f8(System.Resources.RuntimeResourceSet)->
0124aa5c(System.Resources.ResourceReader)->
0124aaac(System.IO.BinaryReader)->
0124aaf4(System.Byte[])
0124ab48
Note: Roots found on stacks may be false positives. Run "!help gcroot" for more info.
Scan Thread 0 OSTHread 1194
Scan Thread 2 OSTHread 1720
DOMAIN(00149768):HANDLE(Pinned):3e13fc:Root:02241010(System.Object[])->
0124a1c4(System.Environment+ResourceHelper)->
0124a358(System.Resources.ResourceManager)->
0124a3a0(System.Collections.Hashtable)->
0124a3d8(System.Collections.Hashtable+bucket[])->
0124a9f8(System.Resources.RuntimeResourceSet)->
0124aa5c(System.Resources.ResourceReader)->
0124aaac(System.IO.BinaryReader)->
0124ab48(System.Byte[])
--------------- remainder of output snipped --------------

By using the /ps parameter to .foreach (see Windbg help file for details) you can run gcroot on every nth item to get a sample of objects.