Как заставить .NET агрессивно собирать мусор? - PullRequest
40 голосов
/ 19 мая 2010

У меня есть приложение, которое используется для обработки изображений, и я нахожу себя обычно выделяющим массивы в размере usxort 4000x4000, а также случайное смещение и тому подобное. В настоящее время .NET Framework имеет тенденцию к аварийному завершению в этом приложении, по-видимому, случайно, почти всегда с ошибкой нехватки памяти. 32 МБ не является огромным объявлением, но если .NET фрагментирует память, то вполне возможно, что такие большие непрерывные распределения не будут работать, как ожидалось.

Есть ли способ заставить сборщик мусора быть более агрессивным или дефрагментировать память (если в этом проблема)? Я понимаю, что есть вызовы GC.Collect и GC.WaitForPendingFinalizers, и я довольно обильно обсыпал их через мой код, но я все еще получаю ошибки. Это может быть потому, что я вызываю подпрограммы dll, которые часто используют нативный код, но я не уверен. Я просмотрел этот код C ++ и убедился, что любую объявленную мной память я удаляю, но все же я получаю эти C # сбои, так что я почти уверен, что ее там нет. Интересно, могут ли вызовы C ++ мешать GC, оставляя его позади, потому что он когда-то взаимодействовал с собственным вызовом - возможно ли это? Если да, могу ли я отключить эту функцию?

РЕДАКТИРОВАТЬ: Вот очень специфический код, который вызовет сбой. Согласно этому SO вопросу , мне не нужно избавляться от объектов BitmapSource здесь. Вот наивная версия, в ней нет GC. Собирает. Обычно происходит сбой на итерации 4-10 процедуры отмены. Этот код заменяет конструктор в пустом проекте WPF, так как я использую WPF. Я делаю дурацкие действия с битмапсорсером из-за ограничений, которые я объяснил в своем ответе на @dthorpe ниже, а также требований, перечисленных в этом вопросе 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
        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. Для меня это обычно происходит около х = 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);
        }
    }
}

Если я неправильно обращаюсь с памятью в любом из сценариев, если я что-то обнаружу с помощью профилировщика, дайте мне знать. Там довольно простая рутина.

К сожалению, похоже, ответ @ Кевина верный - это ошибка в .NET и то, как .NET обрабатывает объекты размером более 85 КБ. Эта ситуация кажется мне чрезвычайно странной; Можно ли переписать Powerpoint в .NET с таким ограничением или в любом другом приложении Office? 85k не кажется мне большим пространством, и я также думаю, что любая программа, которая часто использует так называемые «большие» выделения, может стать нестабильной в течение нескольких дней или недель при использовании .NET.

EDIT : Похоже, Кевин прав, это ограничение GC .NET. Для тех, кто не хочет следовать всему потоку, в .NET есть четыре кучи GC: gen0, gen1, gen2 и LOH (куча больших объектов). Все, что 85k или меньше, идет в одну из первых трех куч, в зависимости от времени создания (перемещено от gen0 к gen1 к gen2 и т. Д.). Объекты размером более 85 тыс. Размещаются на LOH. LOH никогда не уплотняется, поэтому в конечном итоге выделение того типа, который я делаю, в конечном итоге приведет к ошибке OOM, так как объекты разбросаны по этому пространству памяти. Мы обнаружили, что переход на .NET 4.0 действительно помогает решить проблему, задерживая исключение, но не предотвращая его. Честно говоря, это похоже на барьер 640 Кб - 85 Кбайт должно хватить для любого пользовательского приложения (перефразируя это видео обсуждения GC в .NET). Напомним, что Java не демонстрирует такого поведения со своим GC.

Ответы [ 12 ]

22 голосов
/ 19 мая 2010

Начните с того, чтобы сузить суть проблемы. Если у вас есть собственная утечка памяти, тыкать в GC ничего не сделает для вас.

Запустите perfmon и посмотрите на размер кучи .NET и счетчики личных байтов. Если размер кучи остается довольно постоянным, но количество личных байтов растет, то у вас есть проблема с собственным кодом, и вам нужно будет использовать инструменты C ++ для его отладки.

Предполагая, что проблема в куче .NET, вы должны запустить профилировщик для кода, такого как Ant-профилировщик Redgate или JetBrain DotTrace. Это скажет вам, какие объекты занимают место и не собираются быстро. Вы также можете использовать WinDbg с SOS для этого, но это довольно сложный интерфейс (хотя и мощный).

Как только вы нашли оскорбительные предметы, должно быть более очевидно, как с ними обращаться. Некоторым видом проблем, вызывающих проблемы, являются статические поля, ссылающиеся на объекты, обработчики событий, не являющиеся незарегистрированными, объекты, живущие достаточно долго, чтобы попасть в Gen2, но затем умирающие вскоре после этого, и т. Д. И т. Д. Без профиля кучи памяти вы не будете возможность точно определить ответ.

Что бы вы ни делали, "либерально разбрызгивание" вызовов GC.Collect - почти всегда неправильный способ попытаться решить проблему.

Существует внешняя вероятность того, что переключение на серверную версию GC улучшит ситуацию (просто свойство в файле конфигурации) - версия рабочей станции по умолчанию ориентирована на поддержание отзывчивости пользовательского интерфейса, поэтому эффективно откажется от больших и длинных бегущие подборки.

22 голосов
/ 19 мая 2010

Вот несколько статей, подробно описывающих проблемы с кучей больших объектов. Это звучит как то, с чем вы можете столкнуться.

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

Опасности кучи большого объекта:
http://www.simple -talk.com / DotNet / .net-каркасные /-опасности-оф-большого объекта кучи /

Вот ссылка на сбор данных о куче больших объектов (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

Простой способ решения этой проблемы состоит в создании небольших объектов, если это вообще возможно. Другой вариант - создать только несколько крупных объектов и использовать их снова и снова. Не идеальная ситуация, но это может быть лучше, чем переписать структуру объекта. Так как вы сказали, что созданные объекты (массивы) имеют разные размеры, это может быть сложно, но это может предотвратить сбой приложения.

4 голосов
/ 19 мая 2010

Используйте Process Explorer (из Sysinternals), чтобы увидеть, что такое куча больших объектов для вашего приложения. Ваша лучшая ставка будет делать ваши массивы меньше, но иметь больше их. Если вы можете избежать размещения ваших объектов в LOH, вы не получите исключений OutOfMemoryException и вам не придется также вызывать GC.Collect вручную.

LOH не уплотняется, а только выделяет новые объекты в конце, что означает, что вы можете достаточно быстро исчерпать пространство.

3 голосов
/ 19 мая 2010

Если вы выделяете большой объем памяти в неуправляемой библиотеке (то есть памяти, о которой GC не знает), то вы можете сделать GC осведомленным об этом с помощью GC.AddMemoryPressure метод.

Конечно, это в некоторой степени зависит от того, что делает неуправляемый код. Вы специально не указали, что он выделяет память, но у меня сложилось впечатление, что это так. Если так, то это именно то, для чего был разработан этот метод. С другой стороны, если неуправляемая библиотека выделяет много памяти, то также возможно, что она фрагментирует память, что полностью вне контроля GC даже при AddMemoryPressure. Надеюсь, что это не так; если это так, вам, вероятно, придется реорганизовать библиотеку или изменить способ ее использования.

P.S. Не забудьте вызвать GC.RemoveMemoryPressure , когда вы наконец освободите неуправляемую память.

(PPS. Некоторые другие ответы, вероятно, верны, скорее всего, это просто утечка памяти в вашем коде; особенно если это обработка изображений, я бы поспорил, что вы неправильно утилизируете свой IDIsposable случаев. Но на тот случай, если эти ответы вас никуда не приведут, это еще один путь, по которому вы могли бы пойти.)

2 голосов
/ 16 июня 2015

Помимо обработки распределений более удобным для GC способом (например, повторное использование массивов и т. Д.), Теперь есть новая опция: вы можете вручную вызвать сжатие LOH.

GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;

Это приведет к уплотнению LOH в следующий раз, когда произойдет сбор gen-2 (либо сам по себе, либо при явном вызове GC.Collect).

Обратите внимание, что не сжатие LOH - это, как правило, хорошая идея - просто ваш сценарий является достаточно приличным случаем для ручного уплотнения. LOH обычно используется для огромных, долгоживущих объектов - например, предварительно выделенных буферов, которые повторно используются во времени и т. Д.

Если ваша версия .NET пока не поддерживает это, вы также можете попытаться выделить в размерах степени, равной двум, вместо того, чтобы выделять именно тот объем памяти, который вам необходим. Это то, что делают многие встроенные распределители, чтобы гарантировать, что фрагментация памяти не станет невозможной глупостью (она в основном устанавливает верхний предел максимальной фрагментации кучи). Это раздражает, но если вы можете ограничить код, который обрабатывает это, небольшой частью вашего кода, это достойный обходной путь.

Обратите внимание, что вам все равно нужно убедиться, что на самом деле можно сжать кучу - любая закрепленная память предотвратит сжатие в куче, в которой она живет.

Еще одна полезная опция - это использование подкачки страниц - никогда не выделяйте более чем, скажем, 64 КБ непрерывного пространства в куче; это означает, что вы будете избегать использования LOH полностью. В вашем случае это не так сложно сделать с помощью простой «обертки-массива». Ключ должен поддерживать хороший баланс между требованиями к производительности и разумной абстракцией.

И, конечно, в крайнем случае, вы всегда можете использовать небезопасный код. Это дает вам большую гибкость в обработке выделения памяти (хотя это немного более болезненно, чем при использовании, например, C ++) - в том числе позволяет вам явно выделять неуправляемую память, выполнять с ней работу и освобождать память вручную. Опять же, это имеет смысл, только если вы можете изолировать этот код от небольшой части вашей общей кодовой базы - и убедиться, что у вас есть безопасная управляемая оболочка для памяти, включая соответствующий финализатор (для поддержания некоторого приличного уровня безопасности памяти) , В C # это не так сложно, хотя, если вы обнаружите, что делаете это слишком часто, было бы неплохо использовать C ++ / CLI для этих частей кода и вызывать их из своего кода C #.

2 голосов
/ 19 мая 2010

Просто в стороне: сборщик мусора .NET выполняет «быстрый» сборщик мусора, когда функция возвращается к своему вызывающему. Это позволит удалить локальные переменные, объявленные в функции.

Если вы структурируете свой код так, что у вас есть одна большая функция, которая снова и снова выделяет большие блоки в цикле, назначая каждый новый блок одной и той же локальной переменной, GC может не включиться, чтобы вернуть блоки, на которые нет ссылок, в течение некоторого времени .

Если, с другой стороны, вы структурируете свой код так, что у вас есть внешняя функция с циклом, который вызывает внутреннюю функцию, а память выделяется и присваивается локальной переменной в этой внутренней функции, GC должен запустить немедленно, когда внутренняя функция возвращается к вызывающей стороне и возвращает большой блок памяти, который был только что выделен, потому что это локальная переменная в функции, которая возвращает.

Избегайте соблазна явно связываться с GC.Collect.

1 голос
/ 19 мая 2010

Вы можете реализовать свой собственный класс массива, который разбивает память на несмежные блоки. Скажем, есть массив 64 на 64 [64,64] ushort-массивов, которые выделяются и освобождаются отдельно. Затем просто сопоставьте карту с правой. Местоположение 66,66 будет в расположении [2,2] в массиве в [1,1].

Тогда вы сможете уклоняться от кучи больших объектов.

1 голос
/ 19 мая 2010

Вы проверяли на утечки памяти? Я использовал .NET Memory Profiler с небольшим успехом в проекте, который имел ряд очень тонких и раздражающих постоянных (каламбур) утечек памяти.

Так же, как проверка работоспособности, убедитесь, что вы вызываете Dispose для любых объектов, которые реализуют IDisposable.

0 голосов
/ 20 октября 2016

GC не учитывает неуправляемую кучу. Если вы создаете множество объектов, которые являются просто обертками в C#, для большей неуправляемой памяти, то ваша память пожирается, но GC не может принимать рациональные решения, основанные на этом, поскольку она видит только управляемую кучу.

Вы попадаете в ситуацию, когда GC не думает, что вам не хватает памяти, потому что большинство вещей в вашей куче 1-го поколения - это 8-байтовые ссылки, где на самом деле они похожи на айсберги в море. Большая часть памяти ниже!

Вы можете использовать эти GC звонки:

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

Эти методы позволяют GC видеть неуправляемую память (если вы предоставите ей правильные цифры).

0 голосов
/ 15 января 2014

Вы можете использовать этот метод:

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

три способа использования этого метода.

1 - после удаления управляемого объекта, такого как класс, ....

2 - создать таймер с такими 2000 интервалами.

3 - создать поток для вызова этого метода.

Я предлагаю вам использовать этот метод в потоке или таймере.

...