How do I get .NET to garbage collect aggressively?

2019-01-16 14:08发布

I have an application that is used in image processing, and I find myself typically allocating arrays in the 4000x4000 ushort size, as well as the occasional float and the like. Currently, the .NET framework tends to crash in this app apparently randomly, almost always with an out of memory error. 32mb is not a huge declaration, but if .NET is fragmenting memory, then it's very possible that such large continuous allocations aren't behaving as expected.

Is there a way to tell the garbage collector to be more aggressive, or to defrag memory (if that's the problem)? I realize that there's the GC.Collect and GC.WaitForPendingFinalizers calls, and I've sprinkled them pretty liberally through my code, but I'm still getting the errors. It may be because I'm calling dll routines that use native code a lot, but I'm not sure. I've gone over that C++ code, and make sure that any memory I declare I delete, but still I get these C# crashes, so I'm pretty sure it's not there. I wonder if the C++ calls could be interfering with the GC, making it leave behind memory because it once interacted with a native call-- is that possible? If so, can I turn that functionality off?

EDIT: Here is some very specific code that will cause the crash. According to this SO question, I do not need to be disposing of the BitmapSource objects here. Here is the naive version, no GC.Collects in it. It generally crashes on iteration 4 to 10 of the undo procedure. This code replaces the constructor in a blank WPF project, since I'm using WPF. I do the wackiness with the bitmapsource because of the limitations I explained in my answer to @dthorpe below as well as the requirements listed in this SO question.

public partial class Window1 : Window {
    public Window1() {
        InitializeComponent();
        //Attempts to create an OOM crash
        //to do so, mimic minute croppings of an 'image' (ushort array), and then undoing the crops
        int theRows = 4000, currRows;
        int theColumns = 4000, currCols;
        int theMaxChange = 30;
        int i;
        List<ushort[]> theList = new List<ushort[]>();//the list of images in the undo/redo stack
        byte[] displayBuffer = null;//the buffer used as a bitmap source
        BitmapSource theSource = null;
        for (i = 0; i < theMaxChange; i++) {
            currRows = theRows - i;
            currCols = theColumns - i;
            theList.Add(new ushort[(theRows - i) * (theColumns - i)]);
            displayBuffer = new byte[theList[i].Length];
            theSource = BitmapSource.Create(currCols, currRows,
                    96, 96, PixelFormats.Gray8, null, displayBuffer,
                    (currCols * PixelFormats.Gray8.BitsPerPixel + 7) / 8);
            System.Console.WriteLine("Got to change " + i.ToString());
            System.Threading.Thread.Sleep(100);
        }
        //should get here.  If not, then theMaxChange is too large.
        //Now, go back up the undo stack.
        for (i = theMaxChange - 1; i >= 0; i--) {
            displayBuffer = new byte[theList[i].Length];
            theSource = BitmapSource.Create((theColumns - i), (theRows - i),
                    96, 96, PixelFormats.Gray8, null, displayBuffer,
                    ((theColumns - i) * PixelFormats.Gray8.BitsPerPixel + 7) / 8);
            System.Console.WriteLine("Got to undo change " + i.ToString());
            System.Threading.Thread.Sleep(100);
        }
    }
}

Now, if I'm explicit in calling the garbage collector, I have to wrap the entire code in an outer loop to cause the OOM crash. For me, this tends to happen around x = 50 or so:

public partial class Window1 : Window {
    public Window1() {
        InitializeComponent();
        //Attempts to create an OOM crash
        //to do so, mimic minute croppings of an 'image' (ushort array), and then undoing the crops
        for (int x = 0; x < 1000; x++){
            int theRows = 4000, currRows;
            int theColumns = 4000, currCols;
            int theMaxChange = 30;
            int i;
            List<ushort[]> theList = new List<ushort[]>();//the list of images in the undo/redo stack
            byte[] displayBuffer = null;//the buffer used as a bitmap source
            BitmapSource theSource = null;
            for (i = 0; i < theMaxChange; i++) {
                currRows = theRows - i;
                currCols = theColumns - i;
                theList.Add(new ushort[(theRows - i) * (theColumns - i)]);
                displayBuffer = new byte[theList[i].Length];
                theSource = BitmapSource.Create(currCols, currRows,
                        96, 96, PixelFormats.Gray8, null, displayBuffer,
                        (currCols * PixelFormats.Gray8.BitsPerPixel + 7) / 8);
            }
            //should get here.  If not, then theMaxChange is too large.
            //Now, go back up the undo stack.
            for (i = theMaxChange - 1; i >= 0; i--) {
                displayBuffer = new byte[theList[i].Length];
                theSource = BitmapSource.Create((theColumns - i), (theRows - i),
                        96, 96, PixelFormats.Gray8, null, displayBuffer,
                        ((theColumns - i) * PixelFormats.Gray8.BitsPerPixel + 7) / 8);
                GC.WaitForPendingFinalizers();//force gc to collect, because we're in scenario 2, lots of large random changes
                GC.Collect();
            }
            System.Console.WriteLine("Got to changelist " + x.ToString());
            System.Threading.Thread.Sleep(100);
        }
    }
}

If I'm mishandling memory in either scenario, if there's something I should spot with a profiler, let me know. That's a pretty simple routine there.

Unfortunately, it looks like @Kevin's answer is right-- this is a bug in .NET and how .NET handles objects larger than 85k. This situation strikes me as exceedingly strange; could Powerpoint be rewritten in .NET with this kind of limitation, or any of the other Office suite applications? 85k does not seem to me to be a whole lot of space, and I'd also think that any program that uses so-called 'large' allocations frequently would become unstable within a matter of days to weeks when using .NET.

EDIT: It looks like Kevin is right, this is a limitation of .NET's GC. For those who don't want to follow the entire thread, .NET has four GC heaps: gen0, gen1, gen2, and LOH (Large Object Heap). Everything that's 85k or smaller goes on one of the first three heaps, depending on creation time (moved from gen0 to gen1 to gen2, etc). Objects larger than 85k get placed on the LOH. The LOH is never compacted, so eventually, allocations of the type I'm doing will eventually cause an OOM error as objects get scattered about that memory space. We've found that moving to .NET 4.0 does help the problem somewhat, delaying the exception, but not preventing it. To be honest, this feels a bit like the 640k barrier-- 85k ought to be enough for any user application (to paraphrase this video of a discussion of the GC in .NET). For the record, Java does not exhibit this behavior with its GC.

12条回答
劳资没心,怎么记你
2楼-- · 2019-01-16 14:30

Have you tested for memory leaks? I've been using .NET Memory Profiler with quite a bit of success on a project that had a number of very subtle and annoyingly persistent (pun intended) memory leaks.

Just as a sanity check, ensure that you're calling Dispose on any objects that implement IDisposable.

查看更多
狗以群分
3楼-- · 2019-01-16 14:31

Start by narrowing down where the problem lies. If you have a native memory leak, poking the GC is not going to do anything for you.

Run up perfmon and look at the .NET heap size and Private Bytes counters. If the heap size remains fairly constant but private bytes is growing then you've got a native code issue and you'll need to break out the C++ tools to debug it.

Assuming the problem is with the .NET heap you should run a profiler against the code like Redgate's Ant profiler or JetBrain's DotTrace. This will tell you which objects are taking up the space and not being collected quickly. You can also use WinDbg with SOS for this but it's a fiddly interface (powerful though).

Once you've found the offending items it should be more obvious how to deal with them. Some of the sort of things that cause problems are static fields referencing objects, event handlers not being unregistered, objects living long enough to get into Gen2 but then dying shortly after, etc etc. Without a profile of the memory heap you won't be able to pinpoint the answer.

Whatever you do though, "liberally sprinkling" GC.Collect calls is almost always the wrong way to try and solve the problem.

There is an outside chance that switching to the server version of the GC would improve things (just a property in the config file) - the default workstation version is geared towards keeping a UI responsive so will effectively give up with large, long running colections.

查看更多
家丑人穷心不美
4楼-- · 2019-01-16 14:31

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 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 GC to see the unmanaged memory (if you provide it the right figures).

查看更多
Ridiculous、
5楼-- · 2019-01-16 14:33

Apart from handling the allocations in a more GC-friendly way (e.g. reusing arrays etc.), there's a new option now: you can manually cause compaction of the LOH.

GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;

This will cause a LOH compaction the next time a gen-2 collection happens (either on its own, or by your explicit call of GC.Collect).

Do note that not compacting the LOH is usually a good idea - it's just that your scenario is a decent enough case for allowing for manual compaction. The LOH is usually used for huge, long-living objects - like pre-allocated buffers that are reused over time etc.

If your .NET version doesn't support this yet, you can also try to allocate in sizes of powers of two, rather than allocating precisely the amount of memory you need. This is what a lot of native allocators do to ensure memory fragmentation doesn't get impossibly stupid (it basically puts an upper limit on the maximum heap fragmentation). It's annoying, but if you can limit the code that handles this to a small portion of your code, it's a decent workaround.

Do note that you still have to make sure it's actually possible to compact the heap - any pinned memory will prevent compaction in the heap it lives in.

Another useful option is to use paging - never allocating more than, say, 64 kiB of contiguous space on the heap; this means you'll avoid using the LOH entirely. It's not too hard to manage this in a simple "array-wrapper" in your case. The key is to maintain a good balance between performance requirements and reasonable abstraction.

And of course, as a last resort, you can always use unsafe code. This gives you a lot of flexibility in handling memory allocations (though it's a bit more painful than using e.g. C++) - including allowing you to explicitly allocate unmanaged memory, do your work with that and release the memory manually. Again, this only makes sense if you can isolate this code to a small portion of your total codebase - and make sure you've got a safe managed wrapper for the memory, including the appropriate finalizer (to maintain some decent level of memory safety). It's not too hard in C#, though if you find yourself doing this too often, it might be a good idea to use C++/CLI for those parts of the code, and call them from your C# code.

查看更多
一纸荒年 Trace。
6楼-- · 2019-01-16 14:37

Here are some articles detailing problems with the Large Object Heap. It sounds like what you might be running into.

http://connect.microsoft.com/VisualStudio/feedback/details/521147/large-object-heap-fragmentation-causes-outofmemoryexception

Dangers of the large object heap:
http://www.simple-talk.com/dotnet/.net-framework/the-dangers-of-the-large-object-heap/

Here is a link on how to collect data on the Large Object Heap (LOH):
http://msdn.microsoft.com/en-us/magazine/cc534993.aspx

According to this, it seems there is no way to compact the LOH. I can't find anything newer that explicitly says how to do it, and so it seems that it hasn't changed in the 2.0 runtime:
http://blogs.msdn.com/maoni/archive/2006/04/18/large-object-heap.aspx

The simple way of handling the issue is to make small objects if at all possible. Your other option to is to create only a few large objects and reuse them over and over. Not an idea situation, but it might be better than re-writing the object structure. Since you did say that the created objects (arrays) are of different sizes, it might be difficult, but it could keep the application from crashing.

查看更多
Melony?
7楼-- · 2019-01-16 14:39

If you're allocating a large amount of memory in an unmanaged library (i.e. memory that the GC isn't aware of), then you can make the GC aware of it with the GC.AddMemoryPressure method.

Of course this depends somewhat on what the unmanaged code is doing. You haven't specifically stated that it's allocating memory, but I get the impression that it is. If so, then this is exactly what that method was designed for. Then again, if the unmanaged library is allocating a lot of memory then it's also possible that it's fragmenting the memory, which is completely beyond the GC's control even with AddMemoryPressure. Hopefully that's not the case; if it is, you'll probably have to refactor the library or change the way in which it's used.

P.S. Don't forget to call GC.RemoveMemoryPressure when you finally free the unmanaged memory.

(P.P.S. Some of the other answers are probably right, this is a lot more likely to simply be a memory leak in your code; especially if it's image processing, I'd wager that you're not correctly disposing of your IDIsposable instances. But just in case those answers don't lead you anywhere, this is another route you could take.)

查看更多
登录 后发表回答