1

I found a strange memory leak in my application and I don't know why it is happening but it drives me crazy. See the example below:

class Program
{
    static void Main(string[] args)
    {
        var dataBuilder = new Program();
        var dataBuilderWeakReference = new WeakReference(dataBuilder);
        /* tons of data being processed */
        dataBuilder = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
        if(dataBuilderWeakReference.IsAlive)
        {
            throw new Exception("Why?");
        }
    }
}

This should work, am I right? The real problem is that the code runs inside a loop and it ends up with OOM exception:

class Program
{
    static void Main(string[] args)
    {
        var dataBuilder = new Program();
        var dataBuilderWeakReference = new WeakReference(dataBuilder);

        while(true /* until there are some data */)
        {
            /* add record to data builder (tons of data) */

            if(true /* data builder is full? */)
            {
                // create new data builder to save data into it
                dataBuilder = null;
                GC.Collect();
                GC.WaitForPendingFinalizers();
                if(dataBuilderWeakReference.IsAlive)
                {
                    throw new Exception("Why?");
                }
                dataBuilder = new Program();
            }
        }
    }
}

I tried profiling the code but the profiler tells me it's held by the main function.

In the example Program class doesn't have any member (for the sake of simplicity), but in the real application it holds references to tons of small (managed) objects and that's where I end up with OOM exception.

Thank you for any kind of help.


Aram Kocharyan suggested running code without 'Prefer 32-bit' option. Shocking result is, that my exception is not thrown i.e. memory is released as expected. BTW: My system is Windows 7 Professional, .NET 4.5, everything up to date.


Sadly my real application uses some mixed assemblies and has to be compiled as X86, thus 'Prefer 32-bit' option is not allowed for me.


OK, I did some tests and my application works if I launch the release build outside Visual Studio. I can see in Task Manager that memory is released as expected. However, It's a mystery to me, why GC behaves so differently in Debug or when Visual Studio is attached. I would expect some problems in release because of some optimizations, but not in debug. Maybe the debugger keeps references to objects longer.

There has to be some logical explanation for this.

SetyCZ
  • 150
  • 7
  • 2
    You can't control the GC in such a way. If it decides to not clean up what you think of as garbage, it won't. Are you actually running out of memory or is bottle neck another resource? – Patrik Jun 25 '15 at 08:21
  • 1
    Is `Program` object larger than `85,000 bytes` ? – bot_insane Jun 25 '15 at 08:24
  • Does it throw an OOM even if you don't call `GC.Collect()`? – Theodoros Chatzigiannakis Jun 25 '15 at 08:26
  • Does one of those ever throw the "Why" exception? You're not clear about that. – H H Jun 25 '15 at 08:27
  • I know this does not answer the question but wouldn't it be better to make your `Program` disposable and use `using`? – Alexander Balabin Jun 25 '15 at 08:31
  • @AlexanderBalabin Not unless it contains a non-memory resource. – Theodoros Chatzigiannakis Jun 25 '15 at 08:32
  • @DavidG - where does he say that? And how do you then get an OOM? – H H Jun 25 '15 at 08:33
  • I'm guessing here, but wouldn't it be possible for the code generator to postpone setting `dataBuilder = null` until the line `dataBuilder = new Program()`? It's not like the language/compiler guarantees anything about garbage collection; even if you call `Collect` you get no guarantees. – MicroVirus Jun 25 '15 at 09:09
  • Related question: [Garbage Collection should have removed object but WeakReference.IsAlive still returning true](http://stackoverflow.com/q/15205891/2718186) – MicroVirus Jun 25 '15 at 09:19
  • @AramKocharyan In the production it is an object that holds lots of references to small objects. The thing is that in the example Program class doesn't have any member and the exception is still thrown. – SetyCZ Jun 25 '15 at 09:30
  • You can't garbage collect on a null object. Test for null before running the garbage collect. – jdweng Jun 25 '15 at 09:34
  • @TheodorosChatzigiannakis Yes. I added GC calls later as an act of pure desperation ;). – SetyCZ Jun 25 '15 at 11:55
  • 1
    Are you absolutely sure you're not keeping references to it? Also, does the same thing happen under Debug and Release? – Theodoros Chatzigiannakis Jun 25 '15 at 12:01
  • @AlexanderBalabin Program class contains only managed resources so I don't think that IDisposable will solve the issue. However, I thought I could implement the interface and manually clean underlying collections, but It's a workaround, I want to know why the hell is this happening ;]. – SetyCZ Jun 25 '15 at 12:01
  • @TheodorosChatzigiannakis I am sure. Check the example, there is no other variable holding the reference to the Program class instance. I tried only Debug. Ants Memory Profiler shows that static void Main() holds all references... – SetyCZ Jun 25 '15 at 12:06
  • 1
    @SetyCZ Under Release, the object is collected and not alive anymore at the time of the check. Thus, the exception isn't thrown. This may indicate some intentional difference in the behavior of the GC between Debug and Release, for debugging purposes. – Theodoros Chatzigiannakis Jun 25 '15 at 12:30
  • @SetyCZ Anecdotally I'm reading lots of information stating that `WeakReference.IsAlive` can only be trusted if it returns false, have you seen these articles and considered them in your situation? Alternatively, if you are loading lots of data temporarily into memory, and then needing to flush it (hence the GC calls), instead of collecting the prior objects, try re-using them such as a re-usable array. – Adam Houldsworth Jun 25 '15 at 12:32
  • @SetyCZ Never mind, most of the articles are around race conditions. Can you post a short example that demonstrate the issue? Or post the code that you've demarcated as a comment, it is very likely that a reference has been leaked somewhere so it is not eligible. – Adam Houldsworth Jun 25 '15 at 12:44
  • @TheodorosChatzigiannakis You are right, the example works as expected when Release is run and only outside Visual Studion (or by Ctrl + F5). This only brings more questions ;]. – SetyCZ Jun 25 '15 at 13:01
  • @SetyCZ Can you confirm that running without the debugger attached causes the behaviour to be as expected? Regardless of release or debug build options. – Adam Houldsworth Jun 25 '15 at 13:35
  • @AdamHouldsworth No, it doesn't work in Debug even without the debugger attached (outside Visual Studio). The only case it works is in Release outside Visual Studion. Release in Visual Studio and Debug in/out Visual Studio throws my exception. – SetyCZ Jun 25 '15 at 13:55
  • @SetyCZ I ask because optimisations will make an item eligible for GC before the method finishes if the variable is not referenced. I think this is only with optimisations enabled. The debugger also does some stuff around making variables and their values available during a debugging session, so I wanted to rule out whether this was the cause or not. The only remaining thing I can think of is the configuration of the GC, what .NET version are you in? Could this simply be a GC race condition? – Adam Houldsworth Jun 25 '15 at 13:58
  • @AdamHouldsworth It's .NET 4.5 application. I have .NET 4.5.2 installed on my computer. I don't think it's a GC race condition. In my opinion it's because the debugger is attached and debug could have some optimization or diagnostic. I will try to contact somebody from Microsoft, I'm sure they will have same simple explanation. – SetyCZ Jun 26 '15 at 11:44
  • @SetyCZ Ok, you've got a better picture of your circumstances than I do, just bear in mind the GC underwent some changes for the 4.5 runtime (see [here](http://blogs.msdn.com/b/dotnet/archive/2012/07/20/the-net-framework-4-5-includes-new-garbage-collector-enhancements-for-client-and-server-apps.aspx)) that involve doing stuff in background threads, which can introduce race conditions with collection and weak references. – Adam Houldsworth Jun 26 '15 at 16:59

2 Answers2

1

It is possible that your object type of Program is allocated in LOH ( Large Objects Heap ).

From MSDN:

The LOH is used for allocating memory for large objects (such as arrays) that require more than 85,000 bytes.

To Force GC to compact LOH when calling collect() you need to set the GCSettings.LargeObjectHeapCompactionMode property to CLargeObjectHeapCompactionMode.CompactOnce like this:

GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();     

Without this flag GC collects this object but will not compact LOH and for result LOH will be fragmented. ( some memmory parts will not be usable )

You can read more about it here.

Note that this property is available from .NET Framework 4.5.1 .

bot_insane
  • 2,545
  • 18
  • 40
  • Hi, thanks. In production code Program class isn't that big, but it holds lots of small objects. But the thing is, that in the example Program class doesn't have any member at all and the exception is still thrown. – SetyCZ Jun 25 '15 at 09:26
  • You are about `OOM` exception or your exception: `throw new Exception("Why?");` ? – bot_insane Jun 25 '15 at 09:29
  • If I create Program class and fill it with data, the OOM exception is thrown. The provided example is simplified, but the idea is same, the Program class instance is not destroyed and my exception is thrown. – SetyCZ Jun 25 '15 at 11:51
  • Yes, but it doesn't help :( . I am now testing this sample and I am getting this problem too..I will try to handle it and update my answer. – bot_insane Jun 25 '15 at 12:11
  • @TheodorosChatzigiannakis It's interesting, but like Aram pointed out, the instances are not allocated in LOH. In fact, Program class in the example doesn't have any member. In the real application it holds references to lots of small objects. So I believe it's not the case. – SetyCZ Jun 25 '15 at 12:18
  • @SetyCZ Can you try to compile your program without `Prefered 32-bit` flag and post results? And what system do you run ? – bot_insane Jun 25 '15 at 12:21
  • @AramKocharyan See the result in the original post. It's shocking. – SetyCZ Jun 25 '15 at 12:56
  • @SetyCZ Shocking is that in Windows Server 2008 X64 it works in any case. ( also with prefered 32-bit checked ) . As For your question you can compile your poject under x32 system, it should work. ( I tested it. ) – bot_insane Jun 25 '15 at 13:26
0

One thing I would suggest is to redesigning your application to reuse the same Program instance by Resetting/Clearing it.

Microsoft's Large Object Heap Improvements in .NET 4.5 article suggests to use Object Pool Pattern for such cases.

Even though WeakReference is an handy option, it's not quite suitable for large objects. .Net allow breaks downs its application memory heap into 4 different chuncks. Small Object Heap (Generation 0, 1, 2) and Large Object Heap. If SOH or LOH runs out of memory it throws out of memory exception. LOH is quite tricky as it can be badly defragmented when you add/remove objects very often.

Even though GCLargeObjectHeapCompactionMode.CompactOnce option is available now, it's a very bad practice to call it iteratively in such a loop. This is a good option for a server to execute over night or so.

For more details on the dangers of LOH fragmentation,read “The dangers of the Large Object Heap” article.

CharithJ
  • 46,289
  • 20
  • 116
  • 131