Interesting Memory Leak in .Net 3.5 Binary Deserialization

Recently I was analyzing an application written in managed code for memory problems.  In managed code a common cause of eating up memory is statically allocated objects which are not nulled out after they are no longer needed.

In the application I was debugging it was making use of binary deserialization to reconstruct a graph of objects. Many of these objects needed some additional context so a StreamingContext object was created with an additional context state. Here’s a sample of what the code was doing:

    1: using (FileStream fs = myfile.OpenRead())
    2: {
    3:     ContextObject ct = new ContextObject();
    4:     ct.Name = "testing";
    5:     StreamingContext context = new StreamingContext(StreamingContextStates.Other, ct);
    6:     BinaryFormatter bf = new BinaryFormatter();
    7:     bf.Context = context;
    8:     SerializableObject obj = (SerializableObject)bf.Deserialize(fs);
    9: }

Everything was working well until I noticed that deserialization would consistently increase the memory footprint (recognize that, in my case, the additional context object was quite large). I wrote a sample application analyzed the problem using WinDBG. The sample program is located here.

Open WinDBG and select “Open Executable”. Select LeakApplication.exe and hit F5. LeakApplication will run waiting for console input. Select ‘Break’ in WinDBG and type “!dumpheap -type LeakApplication.ContextObject”. You’ll see something like this:

    1: 0:003> !dumpheap -type LeakApplication.ContextObject
    2:  Address       MT     Size
    3: 01f6b7b0 0042340c       12     
    4: total 1 objects
    5: Statistics:
    6:       MT    Count    TotalSize Class Name
    7: 0042340c        1           12 LeakApplication.ContextObject
    8: Total 1 objects

WinDBG is telling us that 1 LeakApplication.ContextObject may be in memory. In order to determine if this is accurate type “!gcroot 01f6b7b0”. Here are the results:

    1: 0:003> !gcroot 01f6b7b0 
    2: Note: Roots found on stacks may be false positives. Run "!help gcroot" for
    3: more info.
    4: Scan Thread 0 OSTHread 12f0
    5: ESP:20ef20:Root:01fcb148(System.Runtime.Serialization.Formatters.Binary.BinaryFormatter)->
    6: 01f6b7b0(LeakApplication.ContextObject)
    7: ESP:20ef3c:Root:01f6b7b0(LeakApplication.ContextObject)->
    8: 01f6b7b0(LeakApplication.ContextObject)
    9: ESP:20ef40:Root:01f6b7bc(System.Runtime.Serialization.Formatters.Binary.BinaryFormatter)->
   10: 01f6b7b0(LeakApplication.ContextObject)
   11: ESP:20ef54:Root:01f6b7b0(LeakApplication.ContextObject)->
   12: 01f6b7b0(LeakApplication.ContextObject)
   13: Scan Thread 2 OSTHread a28
   14: DOMAIN(002F1DE8):HANDLE(Pinned):4013fc:Root:02f51010(System.Object[])->
   15: 01f6ad88(System.Collections.Generic.Dictionary`2[[System.Runtime.Serialization.MemberHolder, mscorlib],[System.Reflection.MemberInfo[], mscorlib]])->
   16: 01f6ae5c(System.Collections.Generic.Dictionary`2+Entry[[System.Runtime.Serialization.MemberHolder, mscorlib],[System.Reflection.MemberInfo[], mscorlib]][])->
   17: 01f6cb5c(System.Runtime.Serialization.MemberHolder)->
   18: 01f6b7b0(LeakApplication.ContextObject)

Sure enough the BinaryFormatter is holding onto the LeakApplication.ContextObject. This is a known leak and, unfortunately, the best you can do is to mitigate the problem. In my case I created a small, disposable property bag object to hold onto my larger “additional” context objects. After deserialization simply call Dispose on the property bag and let it NULL out any members, thus clipping the object graph. Your property bag will still leak, but at least your larger object graph will be available for garbage collection.

-- Patrick