如何让.NET积极收集垃圾?

时间:2010-05-18 20:32:32

标签: c# .net memory-management garbage-collection

我有一个用于图像处理的应用程序,我发现自己通常会分配4000x4000 ushort大小的数组,以及偶尔浮点数等。目前,.NET框架在这个应用程序中往往会随机崩溃,几乎总是出现内存不足错误。 32mb不是一个巨大的声明,但如果.NET碎片化内存,那么这种大型连续分配很可能没有按预期运行。

有没有办法告诉垃圾收集器更具攻击性,或者对内存进行碎片整理(如果这是问题)?我意识到有GC.Collect和GC.WaitForPendingFinalizers调用,我通过我的代码非常自由地散布它们,但我仍然得到错误。这可能是因为我正在调用使用本机代码的dll例程,但我不确定。我已经查看了那个C ++代码,并确保我声明删除的任何内存,但我仍然得到这些C#崩溃,所以我很确定它不存在。我想知道C ++调用是否会干扰GC,使其留下内存,因为它曾经与本机调用交互 - 这可能吗?如果是这样,我可以关闭该功能吗?

编辑:以下是一些非常具体的代码,会导致崩溃。根据{{​​3}},我不需要在这里处理BitmapSource对象。这是天真的版本,没有GC.Collects。它通常在撤销过程的迭代4到10上崩溃。这段代码替换了空白WPF项目中的构造函数,因为我使用的是WPF。由于我在下面对@dthorpe的回答中解释的限制以及this SO question中列出的要求,我使用bitmapsource做了一些古怪的事情。

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);
        }
    }
}

现在,如果我明确地调用垃圾收集器,我必须将整个代码包装在外部循环中以导致OOM崩溃。对我来说,这往往发生在x = 50左右:

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);
        }
    }
}

如果我在任何一种情况下都错误处理了内存,如果我应该发现一些有关探查器的内容,请告诉我。那是一个非常简单的例程。

不幸的是,看起来@ Kevin的答案是正确的 - 这是.NET中的一个错误以及.NET如何处理大于85k的对象。这种情况让我非常奇怪;可以使用这种限制或任何其他Office套件应用程序在.NET中重写Powerpoint吗? 85k在我看来并不是一个很大的空间,而且我还认为任何使用所谓“大”分配的程序在使用.NET时会在几天到几周内变得不稳定。

编辑:看起来凯文是对的,这是.NET的GC的限制。对于那些不想遵循整个线程的人,.NET有四个GC堆:gen0,gen1,gen2和LOH(大对象堆)。根据创建时间(从gen0移动到gen1到gen2等),所有85k或更小的东西都在前三个堆中的一个上。大于85k的物体放置在LOH上。 LOH 从不压缩,所以最终,我正在做的类型的分配最终将导致OOM错误,因为对象散布在该内存空间周围。我们发现迁移到.NET 4.0确实有点帮助了这个问题,延迟了异常,但没有阻止它。说实话,这感觉有点像640k屏障 - 对于任何用户应用程序来说85k应该足够了(用于解释.NET中GC的讨论this SO question)。对于记录,Java不会在其GC中出现此行为。

12 个答案:

答案 0 :(得分:22)

以下是一些详细介绍大对象堆问题的文章。这听起来像你可能遇到的。

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

大型物体堆的危险:
http://www.simple-talk.com/dotnet/.net-framework/the-dangers-of-the-large-object-heap/

这是关于如何收集大对象堆(LOH)数据的链接:
http://msdn.microsoft.com/en-us/magazine/cc534993.aspx

据此,似乎没有办法压缩LOH。我找不到任何明确说明如何做的更新的东西,所以它似乎在2.0运行时没有改变:
http://blogs.msdn.com/maoni/archive/2006/04/18/large-object-heap.aspx

处理问题的简单方法是尽可能制作小物件。您的另一个选择是只创建几个大对象并反复重复使用它们。不是一个想法的情况,但它可能比重写对象结构更好。由于您确实说过创建的对象(数组)大小不同,因此可能很难,但它可能会使应用程序崩溃。

答案 1 :(得分:22)

首先缩小问题所在。如果您有本机内存泄漏,那么戳GC不会为您做任何事情。

运行perfmon并查看.NET堆大小和专用字节计数器。如果堆大小保持相当稳定但私有字节在增长,那么你就会遇到本机代码问题,你需要打破C ++工具来调试它。

假设问题出在.NET堆上,你应该对像Redgate的Ant分析器或JetBrain的DotTrace这样的代码运行一个分析器。这将告诉您哪些对象占用了空间而没有快速收集。您也可以将WinDbg与SOS一起使用,但这是一个繁琐的界面(虽然功能强大)。

一旦找到了违规物品,应该更明白如何处理它们。导致问题的一些事情是引用对象的静态字段,未注册的事件处理程序,生存时间足以进入Gen2但随后很快就会死亡的对象等等。如果没有内存堆的配置文件,您将不会能够找到答案。

无论你做什么,“自由地洒”GC.Collect电话几乎总是尝试解决问题的错误方法。

切换到GC的服务器版本有可能改善一些事情(只是配置文件中的属性) - 默认工作站版本旨在保持UI响应,因此将有效地放弃大,长正在进行的选择。

答案 2 :(得分:4)

使用Process Explorer(来自Sysinternals)查看应用程序的大对象堆是什么。你最好的选择是让你的阵列更小但是拥有更多的阵列。如果您可以避免在LOH上分配对象,那么您将无法获得OutOfMemoryExceptions,也不必手动调用GC.Collect。

LOH不会被压缩,只会在其末尾分配新对象,这意味着你可以很快耗尽空间。

答案 3 :(得分:3)

如果您在非托管库(即GC不知道的内存)中分配大量内存,那么您可以使用{{使 GC意识到它3}}方法。

当然,这在某种程度上取决于非托管代码的作用。你没有明确说明它是分配内存,但我得到的印象是它。如果是这样,那么这正是该方法的设计目标。再说一次,如果非托管库分配了大量内存,那么它也可能会破坏内存,即使使用AddMemoryPressure,这也完全超出了GC的控制范围。希望情况并非如此;如果是,你可能不得不重构库或改变它的使用方式。

P.S。最后释放非托管内存时,不要忘记调用GC.AddMemoryPressure

(PPS其他一些答案可能是正确的,这很可能仅仅是代码中的内存泄漏;特别是如果是图像处理,我打赌你没有正确处理你的{ {1}}实例。但是,如果这些答案没有引导您到任何地方,这是您可以采取的另一条路线。)

答案 4 :(得分:2)

暂且不说:.NET垃圾收集器在函数返回其调用者时执行“快速”GC。这将处理函数中声明的本地变量。

如果您构造代码使得您有一个大型函数在循环中反复分配大块,将每个新块分配给相同的本地var,GC可能无法启动以回收未引用的块一段时间

另一方面,如果构造代码使得外部函数具有调用内部函数的循环,并且内存被分配并分配给该内部函数中的局部变量,则GC应该启动当内部函数返回到调用者并回收刚刚分配的大内存块时立即生成,因为它是正在返回的函数中的局部变量。

避免明确地混淆GC.Collect的诱惑。

答案 5 :(得分:2)

除了以更友好的GC方式处理分配(例如重用数组等)之外,现在还有一个新选项:您可以手动压缩LOH。

GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;

这将在下次发生第2代收集时(或者通过显式调用GC.Collect)导致LOH压缩。

请注意,不压缩LOH 通常是一个好主意 - 只是你的场景足够允许手动压缩。 LOH通常用于巨大的长寿命对象 - 比如预先分配的缓冲区,可以随着时间的推移重复使用等。

如果您的.NET版本尚不支持此功能,您还可以尝试分配2的幂大小,而不是精确分配所需的内存量。这就是许多本机分配器所做的事情,以确保内存碎片不会变得非常愚蠢(它基本上对最大堆碎片设置了一个上限)。这很烦人,但是如果你可以将处理这个代码的代码限制在代码的一小部分,那么这是一个不错的解决方法。

请注意,您仍然必须确保它实际上可以压缩堆 - 任何固定的内存都会阻止它所在的堆中的压缩。

另一个有用的选择是使用分页 - 永远不要在堆上分配超过64 kiB的连续空间;这意味着你将完全避免使用LOH。在你的情况下,在一个简单的“数组包装器”中管理它并不太难。关键是要在性能要求和合理抽象之间保持良好的平衡。

当然,作为最后的手段,您总是可以使用不安全的代码。这为处理内存分配提供了很大的灵活性(虽然它比使用例如C ++更痛苦) - 包括允许您显式分配非托管内存,使用它并使用手动释放内存。同样,只有将这段代码与总代码库的一小部分隔离开来才有意义 - 并确保你有一个安全的内存包装器,包括适当的终结器(以保持一定程度的内存安全性) 。在C#中并不是太难,但如果你发现自己经常这样做,那么对代码的这些部分使用C ++ / CLI可能是个好主意,并从你的C#代码中调用它们。

答案 6 :(得分:1)

您是否测试过内存泄漏?我一直在使用.NET Memory Profiler在一个项目上取得了相当大的成功,该项目有许多非常微妙且烦人的(双关语)内存泄漏。

正如完整性检查一样,确保您在实施Dispose的任何对象上调用IDisposable

答案 7 :(得分:1)

您可以实现自己的数组类,将内存分解为非连续的块。比方说,有一个64乘64的64 [64,64]个ushort数组,它们分别被分配和解除分配。然后只是映射到右边的一个。位置66,66将位于[1,1]阵列中的位置[2,2]。

然后,你应该能够躲避大型物体堆。

答案 8 :(得分:0)

问题很可能是由于你在内存中拥有的这些大对象的数量。如果碎片是可变大小的话,碎片将是一个更可能的问题(虽然它仍然可能是一个问题。)你在评论中说明你在内存中为图像文件存储了一个撤销堆栈。如果将其移动到磁盘,您将节省大量的应用程序内存空间。

同样将撤消移动到磁盘不应该对性能造成太大的负面影响,因为它不是您将一直使用的东西。 (如果它确实成为一个瓶颈,你总是可以创建一个混合磁盘/内存缓存系统。)

<强>扩展...

如果您真的担心将撤消数据存储在文件系统上可能会对性能造成影响,您可能会认为虚拟内存系统很有可能将此数据分页到您的虚拟页面文件中。如果为这些撤消文件创建自己的页面文件/交换空间,则可以控制磁盘I / O的调用时间和位置。不要忘记,即使我们都希望我们的计算机拥有无限的资源,但它们非常有限。

1.5GB(可用应用程序内存空间)/ 32MB(大内存请求大小)〜= 46

答案 9 :(得分:0)

最好的方法是像本文所示,它是西班牙语,但你确定理解代码。 http://www.nerdcoder.com/c-net-forzar-liberacion-de-memoria-de-nuestras-aplicaciones/

这里的代码链接得到brock

using System.Runtime.InteropServices; 
....
public class anyname
{ 
....

[DllImport("kernel32.dll", EntryPoint = "SetProcessWorkingSetSize", ExactSpelling = true, CharSet = CharSet.Ansi, SetLastError = true)]

private static extern int SetProcessWorkingSetSize(IntPtr process, int minimumWorkingSetSize, int maximumWorkingSetSize);

public static void alzheimer()
{
GC.Collect();
GC.WaitForPendingFinalizers();
SetProcessWorkingSetSize(System.Diagnostics.Process.GetCurrentProcess().Handle, -1, -1);
} 

...

你打电话给alzheimer()来清理/释放记忆。

答案 10 :(得分:0)

您可以使用此方法:

public static void FlushMemory()
{
    Process prs = Process.GetCurrentProcess();
    prs.MinWorkingSet = (IntPtr)(300000);
}

使用此方法的三种方法。

1 - 在处理了类,....等管理对象后。

2 - 创建具有这2000个间隔的计时器。

3 - 创建线程来调用此方法。

我建议您在线程或计时器中使用此方法。

答案 11 :(得分:0)

configuration is invalid { "errors":[ { "object":{ "mail_from":"from@example.com", "mail_to":"to@example.com", "use_ami_role":true, "access_key":"accesskey", "secret_key":"secretkey", "region":"us-west-2", "subscriptions":{ "subscription_name":{ "mail_to":"teamemail@example.com" } }, "name":"handler-ses" }, "message":"handler type must be a string" }, { "object":{ "mail_from":"from@example.com", "mail_to":"to@example.com", "use_ami_role":true, "access_key":"accesskey", "secret_key":"secretkey", "region":"us-west-2", "subscriptions":{ "subscription_name":{ "mail_to":"teamemail@example.com" } }, "name":"handler-ses" }, "message":"unknown handler type" } ] } 未考虑非托管堆。如果你创建的对象只是GC中较大的非托管内存的包装器,那么你的内存正在被吞噬,但是C#无法根据此做出合理的决定,因为它只能看到托管堆

你最终会遇到GC不认为你缺乏记忆的情况,因为你的gen 1堆上的大多数东西都是8字节的引用,实际上它们就像海上的冰山一样。大部分记忆都在下面!

您可以使用这些GC来电:

  • 系统:: GC :: AddMemoryPressure(sizeOfField);
  • 系统:: GC :: RemoveMemoryPressure(sizeOfField);

这些方法允许GC查看非托管内存(如果您提供正确的数字)。