9

I am working on a relatively large solution in Visual Studio 2010. It has various projects, one of them being an XNA Game-project, and another one being an ASP.NET MVC 2-project.

With both projects I am facing the same issue: After starting them in debug mode, memory usage keeps rising. They start at 40 and 100MB memory usage respectively, but both climb to 1.5GB relatively quickly (10 and 30 minutes respectively). After that it would sometimes drop back down to close to the initial usage, and other times it would just throw OutOfMemoryExceptions.

Of course this would indicate severe memory leaks, so that is where I initially tried to spot the problem. After searching for leaks unsuccesfully, I tried calling GC.Collect() regularly (about once per 10 seconds). After introducing this "hack", memory usage stayed at 45 and 120MB respectively for 24 hours (until I stopped testing).

.NET's garbage collection is supposed to be "very good", but I can't help suspecting that it just doesn't do its job. I have used CLR Profiler in an attempt to troubleshoot the issue, and it showed that the XNA project seemed to have saved a lot of byte arrays I was indeed using, but to which the references should already be deleted, and therefore collected by the garbage collector.

Again, when I call GC.Collect() regularly, the memory usage issues seem to have gone. Does anyone know what could be causing this high memory usage? Is it possibly related to running in Debug mode?

Jacotb
  • 135
  • 1
  • 1
  • 8
  • 1
    Does the same issue repeat in Release mode? – pickypg Jun 16 '11 at 15:55
  • How big were the individual byte arrays? They might have been put in the Large Object Heap, which is not collected as often as everything else. Was there memory pressure on the system? – agent-j Jun 16 '11 at 15:57
  • 1
    What's the value you're using for "memory usage"? Virtual size? Private bytes? – Sven Jun 16 '11 at 15:59
  • @agent-j LOH objects are collected, but GC does not defragment free space in LOH so the application can suffer from out of memory issues even when there are not so much objects in memory. – Alex Netkachov Jun 16 '11 at 16:07
  • Let's make sure they are indeed big arrays before we jump to conclusions. Unless you want to dig in wit `son of strike` and see which heap they are in. @AlexAtNet, good point about fragmentation. I've never actually seen it, but I've had to look for it once or twice. – agent-j Jun 16 '11 at 16:12
  • @AlexAtNet: Yep, I ran into this only a few weeks ago. A good profiler will tell you in plain terms where the problem is. – Ed S. Jun 16 '11 at 16:18
  • @pickypg & LukeH: Yes, it happens in Release mode too. – Jacotb Jun 16 '11 at 18:46
  • @agent-j: The individual byte arrays contain the 32-bits RGB-data for textures, generated from video frames and image files. So I imagine a single array would be 300kB - 4MB. A new byte array is created for every new texture I am loading, and disposed of (as in, set to null) as soon as the texture has been loaded. – Jacotb Jun 16 '11 at 18:48
  • @Sven: I am afraid I don't exactly know the difference. I have been looking at memory usage in Windows Task Manager. – Jacotb Jun 16 '11 at 18:51

6 Answers6

16

After searching for leaks unsuccesfully

Try harder =)

Memory leaks in a managed language can be tricky to track down. I have had good experiences with the Redgate ANTS Memory Profiler. It's not free, but they give you a 14 day, full-featured trial. It has a nice UI and shows you where you memory is allocated and why these objects are being kept in memory.

As Alex says, event handlers are a very common source of memory leaks in a .NET app. Consider this:

public static class SomeStaticClass
{
    public event EventHandler SomeEvent;
}

private class Foo
{
    public Foo()
    {
        SomeStaticClass.SomeEvent += MyHandler;
    }

    private void MyHandler( object sender, EventArgs ) { /* whatever */ }
}

I used a static class to make the problem as obvious as possible here. Let's say that, during the life of your application, many Foo objects are created. Each Foo subscribes to the SomeEvent event of the static class.

The Foo objects may fall out of scope at one time or another, but the static class maintains a reference to each one via the event handler delegate. Thus, they are kept alive indefinitely. In this case, the event handler simply needs to be "unhooked".

...the XNA project seemed to have saved a lot of byte arrays I was indeed using...

You may be running into fragmentation in the LOH. If you are allocating large objects very frequently they may be causing the problem. The total size of these objects may be much smaller than the total memory allocated to the runtime, but due to fragmentation there is a lot of unused memory allocated to your application.

The profiler I linked to above will tell you if this is a problem. If it is, you will likely be able to track it down to an object leak somewhere. I just fixed a problem in my app showing the same behavior and it was due to a MemoryStream not releasing its internal byte[] even after calling Dispose() on it. Wrapping the stream in a dummy stream and nulling it out fixed the problem.

Also, stating the obvious, make sure to Dispose() of your objects that implement IDisposable. There may be native resources lying around. Again, a good profiler will catch this.

My suggestion; it's not the GC, the problem is in your app. Use a profiler, get your app in a high memory consumption state, take a memory snapshot and start analyzing.

Ed S.
  • 122,712
  • 22
  • 185
  • 265
  • 1
    Thanks, very helpful post. I didn't know about the event handlers preventing objects from being collected, I will definitely look into that. I'll post the results later. – Jacotb Jun 16 '11 at 18:59
  • 1
    I unloaded some event handlers that were still in use, just like you suggested. This brought the memory usage down significantly. Thanks for the help. – Jacotb Jun 18 '11 at 08:15
5

First and foremost, the GC works, and works well. There's no bug in it that you have just discovered.

Now that we've gotten that out of the way, some thoughts:

  1. Are you using too many threads?
  2. Remember that GC is non deterministic; it'll run whenever it thinks it needs to run (even if you call GC.Collect().
  3. Are you sure all your references are going out of scope?
  4. What are you loading into memory in the first place? Large images? Large text files?

Your profiler should tell you what's using so much memory. Start cutting at the biggest culprits as much as you can.

Also, calling GC.Collect() every X seconds is a bad idea and will unlikely solve your real problem.

Esteban Araya
  • 29,284
  • 24
  • 107
  • 141
  • - I am using a number of threads, but not what I would call "too much". I don't think I will have more than 5 threads running at any point. - I know GC basically does whatever it wants to, and GC.Collect() is only a suggestion to GC that it might have some stuff to clean up. However, that makes it even more strange that a Collect() call would solve the memory issues. - My references are definitely going out of scope, I am explicitly setting the byte array references to null whenever I don't need them anymore. - What is using most memory is byte arrays containing 32-bits RGBA texture data. – Jacotb Jun 16 '11 at 18:53
  • 1
    *"My references are definitely going out of scope, I am explicitly setting the byte array references to null whenever I don't need them anymore"* - unless of course there are other references to the byte[] out there. That said, this sounds like LOH fragmentation. A profiler will tell you for sure. – Ed S. Jun 16 '11 at 20:40
4

Analyzing memory issues in .NET is not a trivial task and you definitely should read several good articles and try different tools to achieve the result. I end up with the following article after investigations: http://www.alexatnet.com/content/net-memory-management-and-garbage-collector You can also try to read some of Jeffrey Richter's articles, like this one: http://msdn.microsoft.com/en-us/magazine/bb985010.aspx

From my experience, there are two most common reasons for Out-Of-Memory issue:

  1. Event handlers - they may hold the object even when no other objects referencing it. So ideally you need to unsubscribe event handlers to destroy the object automatically.
  2. Finalizer thread is blocked by some other thread in STA mode. For example, when STA thread does a lot of work the other threads are stopped and the objects that are in the finalization queue cannot be destroyed.
Alex Netkachov
  • 13,172
  • 6
  • 53
  • 85
  • I didn't know that about STA threads. In other words, in WPF if the UI thread is really busy, the GC can't collect? Do you have an article I could read on that? – Phil Jun 16 '11 at 16:08
  • But if manually calling `GC.Collect` solves the problem then it obviously isn't that the GC *can't* collect the objects or that it considers them to be rooted, more that it thinks that collecting them isn't necessary. – LukeH Jun 16 '11 at 16:23
  • @LukeH: Exactly. I really wonder why it thinks collecting them isn't necessary, if the system is throwing OutOfMemoryExceptions... – Jacotb Jun 16 '11 at 19:26
3

Edit: Added link to Large Object Heap fragmentation.

Edit: Since it looks like it is a problem with allocating and throwing away the Textures, can you use Texture2D.SetData to reuse the large byte[]s?

First, you need to figure out whether it is managed or unmanaged memory that is leaking.

  1. Use perfmon to see what happens to your process '.net memory# Bytes in all Heaps' and Process\Private Bytes. Compare the numbers and the memory rises. If the rise in Private bytes outpaces the rise in heap memory, then it's unmanaged memory growth.

  2. Unmanaged memory growth would point to objects that are not being disposed (but eventually collected when their finalizer executes).

  3. If it's managed memory growth, then we'll need to see which generation/LOH (there are also performance counters for each generation of heap bytes).

  4. If it's Large Object Heap bytes, you'll want to reconsider the use and throwing away of large byte arrays. Perhaps the byte arrays can be re-used instead of discarded. Also, consider allocating large byte arrays that are powers of 2. This way, when disposed, you'll leave a large "hole" in the large object heap that can be filled by another object of the same size.

  5. A final concern is pinned memory, but I don't have any advice for you on this because I haven't ever messed with it.

Community
  • 1
  • 1
agent-j
  • 27,335
  • 5
  • 52
  • 79
1

I would also add that if you are doing any file access, make sure that you are closing and/or disposing of any Readers or Writers. You should have a matching 1-1 between opening any file and closing it.

Also, I usually use the using clause for resources, like a Sql Connection:

using (var connection = new SqlConnection())
{
  // Do sql connection work in here.
}

Are you implementing IDisposable on any objects and possibly doing something custom that is causing any issues? I would double check all of your IDisposable code.

Jonathan Nixon
  • 4,940
  • 5
  • 39
  • 53
  • Good advice in general, but `IDisposable` has nothing to do with the deallocation of managed memory. – LukeH Jun 16 '11 at 16:26
  • Yes I completely agree with you there. But I wouldn't assume that they are using only managed resources. In fact, as soon as I read XNA game I immediately wondered about what unmanaged resources they might be using. – Jonathan Nixon Jun 16 '11 at 17:23
  • Closing IO streams is something I always do - but thanks for the pointer anyhow. – Jacotb Jun 16 '11 at 19:01
0

The GC doesn't take into account the unmanaged heap. If you are creating lots of objects that are merely wrappers in C# to larger unmanaged memory then your memory is being devoured but the GC can't make rational decisions based on this as it only see the managed heap.

You end up in a situation where the GC collector doesn't think you are short of memory because most of the things on your gen 1 heap are 8 byte references where in actual fact they are like icebergs at sea. Most of the memory is below!

You can make use of these GC calls:

System::GC::AddMemoryPressure(sizeOfField);
System::GC::RemoveMemoryPressure(sizeOfField);

These methods allow the garbage collector to see the unmanaged memory (if you provide it the right figures)